Feat/bulk operations for inventory service (#4503)
* initial push * bulk delete reservations by location ids * add method to interface (not implemented yet) * bulk update * delete reservations by location id bulk * add create bulk for inventory item * refactor attach inventory item method * add changeset * verbose false * method override instead of multiple methods * change up method signature * redo changes when updating interface * update createInventoryLevel method * rename variables * fix feedback * return correct string array when emitting event * refactor inventory service * redo order changes * snapshot * move prep methods
This commit is contained in:
1 parent
d440c834d3
commit
d184d23c63
20 files changed
+1182
-216
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"medusa-plugin-brightpearl": patch
|
||||||
|
"@medusajs/inventory": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa,inventory,types,brightpearl): update some inventory methods to be bulk-operation enabled
|
||||||
@@ -8,7 +8,10 @@ const adminSeeder = require("../../../helpers/admin-seeder")
|
|||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
const { simpleProductFactory } = require("../../../factories")
|
const {
|
||||||
|
simpleProductFactory,
|
||||||
|
simpleOrderFactory,
|
||||||
|
} = require("../../../factories")
|
||||||
const adminHeaders = { headers: { Authorization: "Bearer test_token" } }
|
const adminHeaders = { headers: { Authorization: "Bearer test_token" } }
|
||||||
|
|
||||||
describe("Inventory Items endpoints", () => {
|
describe("Inventory Items endpoints", () => {
|
||||||
@@ -127,7 +130,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("Inventory Items", () => {
|
describe("Inventory Items", () => {
|
||||||
it("Create, update and delete inventory location level", async () => {
|
it("should create, update and delete the inventory location levels", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const inventoryItemId = inventoryItems[0].id
|
const inventoryItemId = inventoryItems[0].id
|
||||||
|
|
||||||
@@ -181,7 +184,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Update inventory item", async () => {
|
it("should update the inventory item", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const inventoryItemId = inventoryItems[0].id
|
const inventoryItemId = inventoryItems[0].id
|
||||||
|
|
||||||
@@ -207,7 +210,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails to update location level to negative quantity", async () => {
|
it("should fail to update the location level to negative quantity", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const inventoryItemId = inventoryItems[0].id
|
const inventoryItemId = inventoryItems[0].id
|
||||||
@@ -241,7 +244,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Retrieve an inventory item", async () => {
|
it("should retrieve the inventory item", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const inventoryItemId = inventoryItems[0].id
|
const inventoryItemId = inventoryItems[0].id
|
||||||
|
|
||||||
@@ -312,7 +315,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Creates an inventory item using the api", async () => {
|
it("should create the inventory item using the api", async () => {
|
||||||
const product = await simpleProductFactory(dbConnection, {})
|
const product = await simpleProductFactory(dbConnection, {})
|
||||||
|
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
@@ -362,7 +365,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
expect(variantInventoryRes.status).toEqual(200)
|
expect(variantInventoryRes.status).toEqual(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("lists location levels based on id param constraint", async () => {
|
it("should list the location levels based on id param constraint", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const inventoryItemId = inventoryItems[0].id
|
const inventoryItemId = inventoryItems[0].id
|
||||||
|
|
||||||
@@ -397,8 +400,9 @@ describe("Inventory Items endpoints", () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("List inventory items", () => {
|
describe("List inventory items", () => {
|
||||||
it("Lists inventory items with location", async () => {
|
it("should list inventory items with location", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
await api.post(
|
await api.post(
|
||||||
@@ -451,7 +455,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Lists inventory items", async () => {
|
it("should list the inventory items", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const inventoryItemId = inventoryItems[0].id
|
const inventoryItemId = inventoryItems[0].id
|
||||||
|
|
||||||
@@ -539,21 +543,21 @@ describe("Inventory Items endpoints", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Lists inventory items searching by title, description and sku", async () => {
|
it("should list the inventory items searching by title, description and sku", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const inventoryService = appContainer.resolve("inventoryService")
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
await Promise.all([
|
await inventoryService.createInventoryItems([
|
||||||
inventoryService.createInventoryItem({
|
{
|
||||||
title: "Test Item",
|
title: "Test Item",
|
||||||
}),
|
},
|
||||||
inventoryService.createInventoryItem({
|
{
|
||||||
description: "Test Desc",
|
description: "Test Desc",
|
||||||
}),
|
},
|
||||||
inventoryService.createInventoryItem({
|
{
|
||||||
sku: "Test Sku",
|
sku: "Test Sku",
|
||||||
}),
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
@@ -585,7 +589,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("When deleting an inventory item it removes associated levels and reservations", async () => {
|
it("should remove associated levels and reservations when deleting an inventory item", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const inventoryService = appContainer.resolve("inventoryService")
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
@@ -650,7 +654,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
expect(inventoryLevelCountPostDelete).toEqual(0)
|
expect(inventoryLevelCountPostDelete).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("When deleting an inventory item it removes the product variants associated to it", async () => {
|
it("should remove the product variant associations when deleting an inventory item", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
await simpleProductFactory(
|
await simpleProductFactory(
|
||||||
@@ -727,5 +731,344 @@ describe("Inventory Items endpoints", () => {
|
|||||||
)
|
)
|
||||||
).toHaveLength(1)
|
).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("inventory service", () => {
|
||||||
|
let inventoryService
|
||||||
|
let productInventoryService
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
productInventoryService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bulk remove the inventory items", async () => {
|
||||||
|
const [items] = await inventoryService.listInventoryItems()
|
||||||
|
|
||||||
|
const ids = items.map((item) => item.id)
|
||||||
|
|
||||||
|
expect(ids).not.toBeFalsy()
|
||||||
|
|
||||||
|
await inventoryService.deleteInventoryItem(ids)
|
||||||
|
|
||||||
|
const [emptyItems] = await inventoryService.listInventoryItems()
|
||||||
|
expect(emptyItems).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bulk create the inventory levels", async () => {
|
||||||
|
const [items] = await inventoryService.listInventoryItems()
|
||||||
|
|
||||||
|
const itemId = items[0].id
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevels([
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const [levels] = await inventoryService.listInventoryLevels({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
})
|
||||||
|
expect(levels).toHaveLength(2)
|
||||||
|
expect(levels).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bulk create the inventory items", async () => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
sku: "sku-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sku: "sku-2",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const createdItems = await inventoryService.createInventoryItems(items)
|
||||||
|
|
||||||
|
expect(createdItems).toHaveLength(2)
|
||||||
|
expect(createdItems).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
sku: "sku-1",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
sku: "sku-2",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bulk delete the inventory levels by location id", async () => {
|
||||||
|
const [items] = await inventoryService.listInventoryItems()
|
||||||
|
|
||||||
|
const itemId = items[0].id
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevels([
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await inventoryService.deleteInventoryItemLevelByLocationId([
|
||||||
|
locationId,
|
||||||
|
location2Id,
|
||||||
|
])
|
||||||
|
|
||||||
|
const [levels] = await inventoryService.listInventoryLevels({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
})
|
||||||
|
expect(levels).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bulk delete the inventory levels by location id", async () => {
|
||||||
|
const [items] = await inventoryService.listInventoryItems()
|
||||||
|
|
||||||
|
const itemId = items[0].id
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevels([
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await inventoryService.deleteInventoryItemLevelByLocationId([
|
||||||
|
locationId,
|
||||||
|
location2Id,
|
||||||
|
])
|
||||||
|
|
||||||
|
const [levels] = await inventoryService.listInventoryLevels({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
})
|
||||||
|
expect(levels).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail to create the reservations with invalid configuration", async () => {
|
||||||
|
const order = await simpleOrderFactory(dbConnection, {
|
||||||
|
line_items: [
|
||||||
|
{ id: "line-item-1", quantity: 1 },
|
||||||
|
{ id: "line-item-2", quantity: 1 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const [items] = await inventoryService.listInventoryItems()
|
||||||
|
|
||||||
|
const itemId = items[0].id
|
||||||
|
|
||||||
|
const error = await inventoryService
|
||||||
|
.createReservationItems([
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
line_item_id: "line-item-1",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
line_item_id: "line-item-2",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.catch((err) => err)
|
||||||
|
|
||||||
|
expect(error.message).toEqual(
|
||||||
|
`Item ${itemId} is not stocked at location ${locationId}, Item ${itemId} is not stocked at location ${locationId}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bulk delete the reservations by their line item ids", async () => {
|
||||||
|
const order = await simpleOrderFactory(dbConnection, {
|
||||||
|
line_items: [
|
||||||
|
{ id: "line-item-1", quantity: 1 },
|
||||||
|
{ id: "line-item-2", quantity: 1 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const [items] = await inventoryService.listInventoryItems()
|
||||||
|
|
||||||
|
const itemId = items[0].id
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createReservationItems([
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
line_item_id: "line-item-1",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
line_item_id: "line-item-2",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const [reservations] = await inventoryService.listReservationItems({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
})
|
||||||
|
expect(reservations).toHaveLength(2)
|
||||||
|
|
||||||
|
await inventoryService.deleteReservationItemsByLineItem([
|
||||||
|
"line-item-1",
|
||||||
|
"line-item-2",
|
||||||
|
])
|
||||||
|
|
||||||
|
const [deletedReservations] =
|
||||||
|
await inventoryService.listReservationItems({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
})
|
||||||
|
expect(deletedReservations).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bulk delete the reservations by their location id", async () => {
|
||||||
|
const order = await simpleOrderFactory(dbConnection, {
|
||||||
|
line_items: [
|
||||||
|
{ id: "line-item-1", quantity: 1 },
|
||||||
|
{ id: "line-item-2", quantity: 1 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const [items] = await inventoryService.listInventoryItems()
|
||||||
|
|
||||||
|
const itemId = items[0].id
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createReservationItems([
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
line_item_id: "line-item-1",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
line_item_id: "line-item-2",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const [reservations] = await inventoryService.listReservationItems({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
})
|
||||||
|
expect(reservations).toHaveLength(2)
|
||||||
|
|
||||||
|
await inventoryService.deleteReservationItemByLocationId([
|
||||||
|
location2Id,
|
||||||
|
locationId,
|
||||||
|
])
|
||||||
|
|
||||||
|
const [deletedReservations] =
|
||||||
|
await inventoryService.listReservationItems({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
})
|
||||||
|
expect(deletedReservations).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bulk update the inventory levels", async () => {
|
||||||
|
const [items] = await inventoryService.listInventoryItems()
|
||||||
|
|
||||||
|
const itemId = items[0].id
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
const levels = await inventoryService.listInventoryLevels({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
})
|
||||||
|
expect(levels).toHaveLength(2)
|
||||||
|
|
||||||
|
await inventoryService.updateInventoryLevels([
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
stocked_quantity: 25,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const [updatedLevels] = await inventoryService.listInventoryLevels({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(updatedLevels).toHaveLength(2)
|
||||||
|
expect(updatedLevels).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 20,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: itemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
stocked_quantity: 25,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -119,7 +119,7 @@ describe("/store/carts", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates an order from a draft order and doesn't adjust reservations", async () => {
|
it("should create the order from a draft order and shouldn't adjust reservations", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
let inventoryItem = await api.get(
|
let inventoryItem = await api.get(
|
||||||
`/admin/inventory-items/${invItemId}`,
|
`/admin/inventory-items/${invItemId}`,
|
||||||
|
|||||||
@@ -542,11 +542,9 @@ describe("/store/carts", () => {
|
|||||||
it("Deletes multiple reservations on successful fulfillment with reservation", async () => {
|
it("Deletes multiple reservations on successful fulfillment with reservation", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const a = await inventoryService.updateInventoryLevel(
|
await inventoryService.updateInventoryLevel(invItemId, locationId, {
|
||||||
invItemId,
|
stocked_quantity: 2,
|
||||||
locationId,
|
})
|
||||||
{ stocked_quantity: 2 }
|
|
||||||
)
|
|
||||||
|
|
||||||
await prodVarInventoryService.reserveQuantity(variantId, 1, {
|
await prodVarInventoryService.reserveQuantity(variantId, 1, {
|
||||||
locationId: locationId,
|
locationId: locationId,
|
||||||
@@ -663,11 +661,9 @@ describe("/store/carts", () => {
|
|||||||
it("Adjusts single reservation on successful fulfillment with over-reserved line item", async () => {
|
it("Adjusts single reservation on successful fulfillment with over-reserved line item", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const a = await inventoryService.updateInventoryLevel(
|
await inventoryService.updateInventoryLevel(invItemId, locationId, {
|
||||||
invItemId,
|
stocked_quantity: 3,
|
||||||
locationId,
|
})
|
||||||
{ stocked_quantity: 3 }
|
|
||||||
)
|
|
||||||
|
|
||||||
await prodVarInventoryService.reserveQuantity(variantId, 3, {
|
await prodVarInventoryService.reserveQuantity(variantId, 3, {
|
||||||
locationId: locationId,
|
locationId: locationId,
|
||||||
@@ -737,11 +733,9 @@ describe("/store/carts", () => {
|
|||||||
stocked_quantity: 3,
|
stocked_quantity: 3,
|
||||||
})
|
})
|
||||||
|
|
||||||
const a = await inventoryService.updateInventoryLevel(
|
await inventoryService.updateInventoryLevel(invItemId, locationId, {
|
||||||
invItemId,
|
stocked_quantity: 3,
|
||||||
locationId,
|
})
|
||||||
{ stocked_quantity: 3 }
|
|
||||||
)
|
|
||||||
|
|
||||||
await prodVarInventoryService.reserveQuantity(variantId, 1, {
|
await prodVarInventoryService.reserveQuantity(variantId, 1, {
|
||||||
locationId: locationId,
|
locationId: locationId,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ describe("Get products", () => {
|
|||||||
invItem = await inventoryService.createInventoryItem({
|
invItem = await inventoryService.createInventoryItem({
|
||||||
sku: "test-sku",
|
sku: "test-sku",
|
||||||
})
|
})
|
||||||
|
|
||||||
await productVariantInventoryService.attachInventoryItem(
|
await productVariantInventoryService.attachInventoryItem(
|
||||||
variantId,
|
variantId,
|
||||||
invItem.id
|
invItem.id
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ describe("Get variant", () => {
|
|||||||
invItem = await inventoryService.createInventoryItem({
|
invItem = await inventoryService.createInventoryItem({
|
||||||
sku: "test-sku",
|
sku: "test-sku",
|
||||||
})
|
})
|
||||||
|
|
||||||
await productVariantInventoryService.attachInventoryItem(
|
await productVariantInventoryService.attachInventoryItem(
|
||||||
variantId,
|
variantId,
|
||||||
invItem.id
|
invItem.id
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ describe("List Variants", () => {
|
|||||||
invItem = await inventoryService.createInventoryItem({
|
invItem = await inventoryService.createInventoryItem({
|
||||||
sku: "test-sku",
|
sku: "test-sku",
|
||||||
})
|
})
|
||||||
|
|
||||||
const invItemId = invItem.id
|
const invItemId = invItem.id
|
||||||
|
|
||||||
await prodVarInventoryService.attachInventoryItem(variantId, invItem.id)
|
await prodVarInventoryService.attachInventoryItem(variantId, invItem.id)
|
||||||
|
|||||||
@@ -296,6 +296,104 @@ describe("Inventory Module", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("updateInventoryLevel", () => {
|
||||||
|
it("should pass along the correct context when doing a bulk update", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
origin_country: "CH",
|
||||||
|
mid_code: "mid code",
|
||||||
|
material: "lycra",
|
||||||
|
weight: 100,
|
||||||
|
length: 200,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
hs_code: "hs_code 123",
|
||||||
|
requires_shipping: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: "location_123",
|
||||||
|
stocked_quantity: 50,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
let error
|
||||||
|
try {
|
||||||
|
await inventoryService.updateInventoryLevels(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: "location_123",
|
||||||
|
stocked_quantity: 25,
|
||||||
|
reserved_quantity: 4,
|
||||||
|
incoming_quantity: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
transactionManager: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
error = e
|
||||||
|
}
|
||||||
|
expect(error.message).toEqual("manager.getRepository is not a function")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should pass along the correct context when doing a single update", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
origin_country: "CH",
|
||||||
|
mid_code: "mid code",
|
||||||
|
material: "lycra",
|
||||||
|
weight: 100,
|
||||||
|
length: 200,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
hs_code: "hs_code 123",
|
||||||
|
requires_shipping: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: "location_123",
|
||||||
|
stocked_quantity: 50,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
let error
|
||||||
|
try {
|
||||||
|
await inventoryService.updateInventoryLevel(
|
||||||
|
inventoryItem.id,
|
||||||
|
"location_123",
|
||||||
|
{
|
||||||
|
stocked_quantity: 25,
|
||||||
|
reserved_quantity: 4,
|
||||||
|
incoming_quantity: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transactionManager: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
error = e
|
||||||
|
}
|
||||||
|
expect(error.message).toEqual("manager.getRepository is not a function")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("deleteInventoryLevel", async () => {
|
it("deleteInventoryLevel", async () => {
|
||||||
const inventoryService = appContainer.resolve("inventoryService")
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { bootstrapApp } = require("../../../helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||||
|
const {
|
||||||
|
simpleProductVariantFactory,
|
||||||
|
simpleProductFactory,
|
||||||
|
} = require("../../factories")
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
describe("Inventory Module", () => {
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
let express
|
||||||
|
|
||||||
|
let invItem1
|
||||||
|
let invItem2
|
||||||
|
let variant1
|
||||||
|
let variant2
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd, verbose: false })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ProductVariantInventoryService", () => {
|
||||||
|
describe("attachInventoryItem", () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const { variants } = await simpleProductFactory(dbConnection, {
|
||||||
|
variants: [{}, {}],
|
||||||
|
})
|
||||||
|
|
||||||
|
variant1 = variants[0]
|
||||||
|
variant2 = variants[1]
|
||||||
|
|
||||||
|
invItem1 = await inventoryService.createInventoryItem({
|
||||||
|
sku: "test-sku-1",
|
||||||
|
})
|
||||||
|
|
||||||
|
invItem2 = await inventoryService.createInventoryItem({
|
||||||
|
sku: "test-sku-2",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should attach the single item with spread params", async () => {
|
||||||
|
const pviService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
await pviService.attachInventoryItem(variant1.id, invItem1.id)
|
||||||
|
|
||||||
|
const variantItems = await pviService.listByVariant(variant1.id)
|
||||||
|
expect(variantItems.length).toEqual(1)
|
||||||
|
expect(variantItems[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: invItem1.id,
|
||||||
|
variant_id: variant1.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should attach multiple inventory items and variants at once", async () => {
|
||||||
|
const pviService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
await pviService.attachInventoryItem([
|
||||||
|
{
|
||||||
|
variantId: variant1.id,
|
||||||
|
inventoryItemId: invItem1.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantId: variant2.id,
|
||||||
|
inventoryItemId: invItem2.id,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const variantItems = await pviService.listByVariant([
|
||||||
|
variant1.id,
|
||||||
|
variant2.id,
|
||||||
|
])
|
||||||
|
expect(variantItems.length).toEqual(2)
|
||||||
|
expect(variantItems).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: invItem1.id,
|
||||||
|
variant_id: variant1.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
variant_id: variant2.id,
|
||||||
|
inventory_item_id: invItem2.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should skip existing attachments when attaching a singular inventory item", async () => {
|
||||||
|
const pviService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
await pviService.attachInventoryItem(variant1.id, invItem1.id)
|
||||||
|
await pviService.attachInventoryItem(variant1.id, invItem1.id)
|
||||||
|
|
||||||
|
const variantItems = await pviService.listByVariant(variant1.id)
|
||||||
|
expect(variantItems.length).toEqual(1)
|
||||||
|
expect(variantItems[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: invItem1.id,
|
||||||
|
variant_id: variant1.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should skip existing attachments when attaching multiple inventory items in bulk", async () => {
|
||||||
|
const pviService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
await pviService.attachInventoryItem(variant1.id, invItem1.id)
|
||||||
|
|
||||||
|
await pviService.attachInventoryItem([
|
||||||
|
{
|
||||||
|
variantId: variant1.id,
|
||||||
|
inventoryItemId: invItem1.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantId: variant2.id,
|
||||||
|
inventoryItemId: invItem2.id,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const variantItems = await pviService.listByVariant([
|
||||||
|
variant1.id,
|
||||||
|
variant2.id,
|
||||||
|
])
|
||||||
|
expect(variantItems.length).toEqual(2)
|
||||||
|
expect(variantItems).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: invItem1.id,
|
||||||
|
variant_id: variant1.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
variant_id: variant2.id,
|
||||||
|
inventory_item_id: invItem2.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail to attach items when a single item has a required_quantity below 1", async () => {
|
||||||
|
const pviService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
|
||||||
|
let e
|
||||||
|
try {
|
||||||
|
await pviService.attachInventoryItem(variant1.id, invItem1.id, 0)
|
||||||
|
} catch (err) {
|
||||||
|
e = err
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(e.message).toEqual(
|
||||||
|
`"requiredQuantity" must be greater than 0, the following entries are invalid: ${JSON.stringify(
|
||||||
|
{
|
||||||
|
variantId: variant1.id,
|
||||||
|
inventoryItemId: invItem1.id,
|
||||||
|
requiredQuantity: 0,
|
||||||
|
}
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pviService.attachInventoryItem([
|
||||||
|
{
|
||||||
|
variantId: variant1.id,
|
||||||
|
inventoryItemId: invItem1.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantId: variant2.id,
|
||||||
|
inventoryItemId: invItem2.id,
|
||||||
|
requiredQuantity: 0,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
} catch (err) {
|
||||||
|
e = err
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(e.message).toEqual(
|
||||||
|
`"requiredQuantity" must be greater than 0, the following entries are invalid: ${JSON.stringify(
|
||||||
|
{
|
||||||
|
variantId: variant2.id,
|
||||||
|
inventoryItemId: invItem2.id,
|
||||||
|
requiredQuantity: 0,
|
||||||
|
}
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail to attach items when attaching to a non-existing variant", async () => {
|
||||||
|
const pviService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
|
||||||
|
let e
|
||||||
|
try {
|
||||||
|
await pviService.attachInventoryItem("variant1.id", invItem1.id)
|
||||||
|
} catch (err) {
|
||||||
|
e = err
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(e.message).toEqual(
|
||||||
|
`Variants not found for the following ids: variant1.id`
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pviService.attachInventoryItem([
|
||||||
|
{
|
||||||
|
variantId: "variant1.id",
|
||||||
|
inventoryItemId: invItem1.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantId: variant2.id,
|
||||||
|
inventoryItemId: invItem2.id,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
} catch (err) {
|
||||||
|
e = err
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(e.message).toEqual(
|
||||||
|
`Variants not found for the following ids: variant1.id`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it("should fail to attach items when attaching to a non-existing inventory item", async () => {
|
||||||
|
const pviService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
|
||||||
|
let e
|
||||||
|
try {
|
||||||
|
await pviService.attachInventoryItem(variant1.id, "invItem1.id")
|
||||||
|
} catch (err) {
|
||||||
|
e = err
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(e.message).toEqual(
|
||||||
|
`Inventory items not found for the following ids: invItem1.id`
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pviService.attachInventoryItem([
|
||||||
|
{
|
||||||
|
variantId: variant1.id,
|
||||||
|
inventoryItemId: invItem1.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantId: variant2.id,
|
||||||
|
inventoryItemId: "invItem2.id",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
} catch (err) {
|
||||||
|
e = err
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(e.message).toEqual(
|
||||||
|
`Inventory items not found for the following ids: invItem2.id`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -75,7 +75,7 @@ export const simpleProductFactory = async (
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const pv of variants) {
|
product.variants = await Promise.all(variants.map(async (pv) => {
|
||||||
const factoryData = {
|
const factoryData = {
|
||||||
...pv,
|
...pv,
|
||||||
product_id: prodId,
|
product_id: prodId,
|
||||||
@@ -85,8 +85,8 @@ export const simpleProductFactory = async (
|
|||||||
{ option_id: optionId, value: faker.commerce.productAdjective() },
|
{ option_id: optionId, value: faker.commerce.productAdjective() },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
await simpleProductVariantFactory(connection, factoryData)
|
return await simpleProductVariantFactory(connection, factoryData)
|
||||||
}
|
}))
|
||||||
|
|
||||||
return product
|
return product
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
MedusaContext,
|
MedusaContext,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { DeepPartial, EntityManager, FindManyOptions } from "typeorm"
|
import { DeepPartial, EntityManager, FindManyOptions, In } from "typeorm"
|
||||||
import { InventoryItem } from "../models"
|
import { InventoryItem } from "../models"
|
||||||
import { getListQuery } from "../utils/query"
|
import { getListQuery } from "../utils/query"
|
||||||
import { buildQuery } from "../utils/build-query"
|
import { buildQuery } from "../utils/build-query"
|
||||||
@@ -120,33 +120,35 @@ export default class InventoryItemService {
|
|||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager()
|
||||||
async create(
|
async create(
|
||||||
data: CreateInventoryItemInput,
|
data: CreateInventoryItemInput[],
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<InventoryItem> {
|
): Promise<InventoryItem[]> {
|
||||||
const manager = context.transactionManager!
|
const manager = context.transactionManager!
|
||||||
const itemRepository = manager.getRepository(InventoryItem)
|
const itemRepository = manager.getRepository(InventoryItem)
|
||||||
|
|
||||||
const inventoryItem = itemRepository.create({
|
const inventoryItem = itemRepository.create(
|
||||||
sku: data.sku,
|
data.map((tc) => ({
|
||||||
origin_country: data.origin_country,
|
sku: tc.sku,
|
||||||
metadata: data.metadata,
|
origin_country: tc.origin_country,
|
||||||
hs_code: data.hs_code,
|
metadata: tc.metadata,
|
||||||
mid_code: data.mid_code,
|
hs_code: tc.hs_code,
|
||||||
material: data.material,
|
mid_code: tc.mid_code,
|
||||||
weight: data.weight,
|
material: tc.material,
|
||||||
length: data.length,
|
weight: tc.weight,
|
||||||
height: data.height,
|
length: tc.length,
|
||||||
width: data.width,
|
height: tc.height,
|
||||||
requires_shipping: data.requires_shipping,
|
width: tc.width,
|
||||||
description: data.description,
|
requires_shipping: tc.requires_shipping,
|
||||||
thumbnail: data.thumbnail,
|
description: tc.description,
|
||||||
title: data.title,
|
thumbnail: tc.thumbnail,
|
||||||
})
|
title: tc.title,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const result = await itemRepository.save(inventoryItem)
|
const result = await itemRepository.save(inventoryItem)
|
||||||
|
|
||||||
await this.eventBusService_?.emit?.(InventoryItemService.Events.CREATED, {
|
await this.eventBusService_?.emit?.(InventoryItemService.Events.CREATED, {
|
||||||
id: result.id,
|
ids: result.map((i) => i.id),
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -195,16 +197,20 @@ export default class InventoryItemService {
|
|||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager()
|
||||||
async delete(
|
async delete(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string | string[],
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const manager = context.transactionManager!
|
const manager = context.transactionManager!
|
||||||
const itemRepository = manager.getRepository(InventoryItem)
|
const itemRepository = manager.getRepository(InventoryItem)
|
||||||
|
|
||||||
await itemRepository.softRemove({ id: inventoryItemId })
|
const ids = Array.isArray(inventoryItemId)
|
||||||
|
? inventoryItemId
|
||||||
|
: [inventoryItemId]
|
||||||
|
|
||||||
|
await itemRepository.softDelete({ id: In(ids) })
|
||||||
|
|
||||||
await this.eventBusService_?.emit?.(InventoryItemService.Events.DELETED, {
|
await this.eventBusService_?.emit?.(InventoryItemService.Events.DELETED, {
|
||||||
id: inventoryItemId,
|
ids: inventoryItemId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,24 +120,28 @@ export default class InventoryLevelService {
|
|||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager()
|
||||||
async create(
|
async create(
|
||||||
data: CreateInventoryLevelInput,
|
data: CreateInventoryLevelInput[],
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<InventoryLevel> {
|
): Promise<InventoryLevel[]> {
|
||||||
const manager = context.transactionManager!
|
const manager = context.transactionManager!
|
||||||
|
|
||||||
|
const toCreate = data.map((d) => {
|
||||||
|
return {
|
||||||
|
location_id: d.location_id,
|
||||||
|
inventory_item_id: d.inventory_item_id,
|
||||||
|
stocked_quantity: d.stocked_quantity,
|
||||||
|
reserved_quantity: d.reserved_quantity,
|
||||||
|
incoming_quantity: d.incoming_quantity,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const levelRepository = manager.getRepository(InventoryLevel)
|
const levelRepository = manager.getRepository(InventoryLevel)
|
||||||
|
|
||||||
const inventoryLevel = levelRepository.create({
|
const inventoryLevels = levelRepository.create(toCreate)
|
||||||
location_id: data.location_id,
|
|
||||||
inventory_item_id: data.inventory_item_id,
|
|
||||||
stocked_quantity: data.stocked_quantity,
|
|
||||||
reserved_quantity: data.reserved_quantity,
|
|
||||||
incoming_quantity: data.incoming_quantity,
|
|
||||||
})
|
|
||||||
|
|
||||||
const saved = await levelRepository.save(inventoryLevel)
|
const saved = await levelRepository.save(inventoryLevels)
|
||||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.CREATED, {
|
await this.eventBusService_?.emit?.(InventoryLevelService.Events.CREATED, {
|
||||||
id: saved.id,
|
ids: saved.map((i) => i.id),
|
||||||
})
|
})
|
||||||
|
|
||||||
return saved
|
return saved
|
||||||
@@ -254,7 +258,7 @@ export default class InventoryLevelService {
|
|||||||
await levelRepository.delete({ id: In(ids) })
|
await levelRepository.delete({ id: In(ids) })
|
||||||
|
|
||||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
||||||
id: inventoryLevelId,
|
ids: inventoryLevelId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,16 +269,18 @@ export default class InventoryLevelService {
|
|||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager()
|
||||||
async deleteByLocationId(
|
async deleteByLocationId(
|
||||||
locationId: string,
|
locationId: string | string[],
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const manager = context.transactionManager!
|
const manager = context.transactionManager!
|
||||||
const levelRepository = manager.getRepository(InventoryLevel)
|
const levelRepository = manager.getRepository(InventoryLevel)
|
||||||
|
|
||||||
await levelRepository.delete({ location_id: locationId })
|
const ids = Array.isArray(locationId) ? locationId : [locationId]
|
||||||
|
|
||||||
|
await levelRepository.delete({ location_id: In(ids) })
|
||||||
|
|
||||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
||||||
location_id: locationId,
|
location_ids: ids,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { InternalModuleDeclaration } from "@medusajs/modules-sdk"
|
import { InternalModuleDeclaration } from "@medusajs/modules-sdk"
|
||||||
import {
|
import {
|
||||||
|
BulkUpdateInventoryLevelInput,
|
||||||
CreateInventoryItemInput,
|
CreateInventoryItemInput,
|
||||||
CreateInventoryLevelInput,
|
CreateInventoryLevelInput,
|
||||||
CreateReservationItemInput,
|
CreateReservationItemInput,
|
||||||
@@ -32,6 +33,7 @@ type InjectedDependencies = {
|
|||||||
inventoryLevelService: InventoryLevelService
|
inventoryLevelService: InventoryLevelService
|
||||||
reservationItemService: ReservationItemService
|
reservationItemService: ReservationItemService
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class InventoryService implements IInventoryService {
|
export default class InventoryService implements IInventoryService {
|
||||||
protected readonly manager_: EntityManager
|
protected readonly manager_: EntityManager
|
||||||
|
|
||||||
@@ -184,10 +186,63 @@ export default class InventoryService implements IInventoryService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureInventoryLevels(
|
||||||
|
data: { location_id: string; inventory_item_id: string }[],
|
||||||
|
context: SharedContext = {}
|
||||||
|
): Promise<InventoryLevelDTO[]> {
|
||||||
|
const inventoryLevels = await this.inventoryLevelService_.list(
|
||||||
|
{
|
||||||
|
inventory_item_id: data.map((e) => e.inventory_item_id),
|
||||||
|
location_id: data.map((e) => e.location_id),
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
const inventoryLevelMap: Map<
|
||||||
|
string,
|
||||||
|
Map<string, InventoryLevelDTO>
|
||||||
|
> = inventoryLevels.reduce((acc, curr) => {
|
||||||
|
const inventoryLevelMap = acc.get(curr.inventory_item_id) ?? new Map()
|
||||||
|
inventoryLevelMap.set(curr.location_id, curr)
|
||||||
|
acc.set(curr.inventory_item_id, inventoryLevelMap)
|
||||||
|
return acc
|
||||||
|
}, new Map())
|
||||||
|
|
||||||
|
const missing = data.filter(
|
||||||
|
(i) => !inventoryLevelMap.get(i.inventory_item_id)?.get(i.location_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (missing.length) {
|
||||||
|
const error = missing
|
||||||
|
.map((missing) => {
|
||||||
|
return `Item ${missing.inventory_item_id} is not stocked at location ${missing.location_id}`
|
||||||
|
})
|
||||||
|
.join(", ")
|
||||||
|
throw new MedusaError(MedusaError.Types.NOT_FOUND, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inventoryLevels.map(
|
||||||
|
(i) => inventoryLevelMap.get(i.inventory_item_id)!.get(i.location_id)!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
|
async createReservationItems(
|
||||||
|
input: CreateReservationItemInput[],
|
||||||
|
@MedusaContext() context: SharedContext = {}
|
||||||
|
): Promise<ReservationItemDTO[]> {
|
||||||
|
await this.ensureInventoryLevels(input, context)
|
||||||
|
|
||||||
|
return await this.reservationItemService_.create(input, context)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a reservation item
|
* Creates a reservation item
|
||||||
* @param input - the input object
|
* @param input - the input object
|
||||||
* @param context
|
|
||||||
* @return The created reservation item
|
* @return The created reservation item
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager(
|
@InjectEntityManager(
|
||||||
@@ -198,29 +253,20 @@ export default class InventoryService implements IInventoryService {
|
|||||||
input: CreateReservationItemInput,
|
input: CreateReservationItemInput,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<ReservationItemDTO> {
|
): Promise<ReservationItemDTO> {
|
||||||
// Verify that the item is stocked at the location
|
const [result] = await this.createReservationItems([input], context)
|
||||||
const [inventoryLevel] = await this.inventoryLevelService_.list(
|
|
||||||
{
|
|
||||||
inventory_item_id: input.inventory_item_id,
|
|
||||||
location_id: input.location_id,
|
|
||||||
},
|
|
||||||
{ take: 1 },
|
|
||||||
context
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!inventoryLevel) {
|
return result
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.NOT_FOUND,
|
|
||||||
`Item ${input.inventory_item_id} is not stocked at location ${input.location_id}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const reservationItem = await this.reservationItemService_.create(
|
@InjectEntityManager(
|
||||||
input,
|
(target) =>
|
||||||
context
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
)
|
)
|
||||||
|
async createInventoryItems(
|
||||||
return { ...reservationItem }
|
input: CreateInventoryItemInput[],
|
||||||
|
@MedusaContext() context: SharedContext = {}
|
||||||
|
): Promise<InventoryItemDTO[]> {
|
||||||
|
return await this.inventoryItemService_.create(input, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,11 +283,20 @@ export default class InventoryService implements IInventoryService {
|
|||||||
input: CreateInventoryItemInput,
|
input: CreateInventoryItemInput,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<InventoryItemDTO> {
|
): Promise<InventoryItemDTO> {
|
||||||
const inventoryItem = await this.inventoryItemService_.create(
|
const [result] = await this.createInventoryItems([input], context)
|
||||||
input,
|
|
||||||
context
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
)
|
)
|
||||||
return { ...inventoryItem }
|
async createInventoryLevels(
|
||||||
|
input: CreateInventoryLevelInput[],
|
||||||
|
@MedusaContext() context: SharedContext = {}
|
||||||
|
): Promise<InventoryLevelDTO[]> {
|
||||||
|
return await this.inventoryLevelService_.create(input, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,7 +313,9 @@ export default class InventoryService implements IInventoryService {
|
|||||||
input: CreateInventoryLevelInput,
|
input: CreateInventoryLevelInput,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<InventoryLevelDTO> {
|
): Promise<InventoryLevelDTO> {
|
||||||
return await this.inventoryLevelService_.create(input, context)
|
const [result] = await this.createInventoryLevels([input], context)
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -295,7 +352,7 @@ export default class InventoryService implements IInventoryService {
|
|||||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
)
|
)
|
||||||
async deleteInventoryItem(
|
async deleteInventoryItem(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string | string[],
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.inventoryLevelService_.deleteByInventoryItemId(
|
await this.inventoryLevelService_.deleteByInventoryItemId(
|
||||||
@@ -311,7 +368,7 @@ export default class InventoryService implements IInventoryService {
|
|||||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
)
|
)
|
||||||
async deleteInventoryItemLevelByLocationId(
|
async deleteInventoryItemLevelByLocationId(
|
||||||
locationId: string,
|
locationId: string | string[],
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return await this.inventoryLevelService_.deleteByLocationId(
|
return await this.inventoryLevelService_.deleteByLocationId(
|
||||||
@@ -325,7 +382,7 @@ export default class InventoryService implements IInventoryService {
|
|||||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
)
|
)
|
||||||
async deleteReservationItemByLocationId(
|
async deleteReservationItemByLocationId(
|
||||||
locationId: string,
|
locationId: string | string[],
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return await this.reservationItemService_.deleteByLocationId(
|
return await this.reservationItemService_.deleteByLocationId(
|
||||||
@@ -362,6 +419,38 @@ export default class InventoryService implements IInventoryService {
|
|||||||
return await this.inventoryLevelService_.delete(inventoryLevel.id, context)
|
return await this.inventoryLevelService_.delete(inventoryLevel.id, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
|
async updateInventoryLevels(
|
||||||
|
updates: ({
|
||||||
|
inventory_item_id: string
|
||||||
|
location_id: string
|
||||||
|
} & UpdateInventoryLevelInput)[],
|
||||||
|
context?: SharedContext
|
||||||
|
): Promise<InventoryLevelDTO[]> {
|
||||||
|
const inventoryLevels = await this.ensureInventoryLevels(updates)
|
||||||
|
|
||||||
|
const levelMap = inventoryLevels.reduce((acc, curr) => {
|
||||||
|
const inventoryLevelMap = acc.get(curr.inventory_item_id) ?? new Map()
|
||||||
|
inventoryLevelMap.set(curr.location_id, curr.id)
|
||||||
|
acc.set(curr.inventory_item_id, inventoryLevelMap)
|
||||||
|
return acc
|
||||||
|
}, new Map())
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
updates.map(async (update) => {
|
||||||
|
const levelId = levelMap
|
||||||
|
.get(update.inventory_item_id)
|
||||||
|
.get(update.location_id)
|
||||||
|
|
||||||
|
// TODO make this bulk
|
||||||
|
return this.inventoryLevelService_.update(levelId, update, context)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an inventory level
|
* Updates an inventory level
|
||||||
* @param inventoryItemId - the id of the inventory item associated with the level
|
* @param inventoryItemId - the id of the inventory item associated with the level
|
||||||
@@ -376,28 +465,21 @@ export default class InventoryService implements IInventoryService {
|
|||||||
)
|
)
|
||||||
async updateInventoryLevel(
|
async updateInventoryLevel(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string,
|
locationIdOrContext?: string,
|
||||||
input: UpdateInventoryLevelInput,
|
input?: UpdateInventoryLevelInput,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<InventoryLevelDTO> {
|
): Promise<InventoryLevelDTO> {
|
||||||
const [inventoryLevel] = await this.inventoryLevelService_.list(
|
const updates: BulkUpdateInventoryLevelInput[] = [
|
||||||
{ inventory_item_id: inventoryItemId, location_id: locationId },
|
{
|
||||||
{ take: 1 },
|
inventory_item_id: inventoryItemId,
|
||||||
context
|
location_id: locationIdOrContext as string,
|
||||||
)
|
...input,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
if (!inventoryLevel) {
|
const [result] = await this.updateInventoryLevels(updates, context)
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.NOT_FOUND,
|
|
||||||
`Inventory level for item ${inventoryItemId} and location ${locationId} not found`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.inventoryLevelService_.update(
|
return result
|
||||||
inventoryLevel.id,
|
|
||||||
input,
|
|
||||||
context
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -131,38 +131,44 @@ export default class ReservationItemService {
|
|||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager()
|
||||||
async create(
|
async create(
|
||||||
data: CreateReservationItemInput,
|
data: CreateReservationItemInput[],
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<ReservationItem> {
|
): Promise<ReservationItem[]> {
|
||||||
const manager = context.transactionManager!
|
const manager = context.transactionManager!
|
||||||
const reservationItemRepository = manager.getRepository(ReservationItem)
|
const reservationItemRepository = manager.getRepository(ReservationItem)
|
||||||
|
|
||||||
const reservationItem = reservationItemRepository.create({
|
const reservationItems = reservationItemRepository.create(
|
||||||
inventory_item_id: data.inventory_item_id,
|
data.map((tc) => ({
|
||||||
line_item_id: data.line_item_id,
|
inventory_item_id: tc.inventory_item_id,
|
||||||
location_id: data.location_id,
|
line_item_id: tc.line_item_id,
|
||||||
quantity: data.quantity,
|
location_id: tc.location_id,
|
||||||
metadata: data.metadata,
|
quantity: tc.quantity,
|
||||||
external_id: data.external_id,
|
metadata: tc.metadata,
|
||||||
description: data.description,
|
external_id: tc.external_id,
|
||||||
created_by: data.created_by,
|
description: tc.description,
|
||||||
})
|
created_by: tc.created_by,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const [newReservationItem] = await Promise.all([
|
const [newReservationItems] = await Promise.all([
|
||||||
reservationItemRepository.save(reservationItem),
|
reservationItemRepository.save(reservationItems),
|
||||||
this.inventoryLevelService_.adjustReservedQuantity(
|
...data.map(
|
||||||
|
async (data) =>
|
||||||
|
// TODO make bulk
|
||||||
|
await this.inventoryLevelService_.adjustReservedQuantity(
|
||||||
data.inventory_item_id,
|
data.inventory_item_id,
|
||||||
data.location_id,
|
data.location_id,
|
||||||
data.quantity,
|
data.quantity,
|
||||||
context
|
context
|
||||||
|
)
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
|
||||||
await this.eventBusService_?.emit?.(ReservationItemService.Events.CREATED, {
|
await this.eventBusService_?.emit?.(ReservationItemService.Events.CREATED, {
|
||||||
id: newReservationItem.id,
|
ids: newReservationItems.map((i) => i.id),
|
||||||
})
|
})
|
||||||
|
|
||||||
return newReservationItem
|
return newReservationItems
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,24 +250,24 @@ export default class ReservationItemService {
|
|||||||
const manager = context.transactionManager!
|
const manager = context.transactionManager!
|
||||||
const itemRepository = manager.getRepository(ReservationItem)
|
const itemRepository = manager.getRepository(ReservationItem)
|
||||||
|
|
||||||
const itemsIds = Array.isArray(lineItemId) ? lineItemId : [lineItemId]
|
const lineItemIds = Array.isArray(lineItemId) ? lineItemId : [lineItemId]
|
||||||
|
|
||||||
const items = await this.list(
|
const reservationItems = await this.list(
|
||||||
{ line_item_id: itemsIds },
|
{ line_item_id: lineItemIds },
|
||||||
undefined,
|
undefined,
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
const ops: Promise<unknown>[] = [
|
const ops: Promise<unknown>[] = [
|
||||||
itemRepository.softDelete({ line_item_id: In(itemsIds) }),
|
itemRepository.softDelete({ line_item_id: In(lineItemIds) }),
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const item of items) {
|
for (const reservation of reservationItems) {
|
||||||
ops.push(
|
ops.push(
|
||||||
this.inventoryLevelService_.adjustReservedQuantity(
|
this.inventoryLevelService_.adjustReservedQuantity(
|
||||||
item.inventory_item_id,
|
reservation.inventory_item_id,
|
||||||
item.location_id,
|
reservation.location_id,
|
||||||
item.quantity * -1,
|
reservation.quantity * -1,
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -281,18 +287,15 @@ export default class ReservationItemService {
|
|||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager()
|
||||||
async deleteByLocationId(
|
async deleteByLocationId(
|
||||||
locationId: string,
|
locationId: string | string[],
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const manager = context.transactionManager!
|
const manager = context.transactionManager!
|
||||||
const itemRepository = manager.getRepository(ReservationItem)
|
const itemRepository = manager.getRepository(ReservationItem)
|
||||||
|
|
||||||
await itemRepository
|
const ids = Array.isArray(locationId) ? locationId : [locationId]
|
||||||
.createQueryBuilder("reservation_item")
|
|
||||||
.softDelete()
|
await itemRepository.softDelete({ location_id: In(ids) })
|
||||||
.where("location_id = :locationId", { locationId })
|
|
||||||
.andWhere("deleted_at IS NULL")
|
|
||||||
.execute()
|
|
||||||
|
|
||||||
await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
|
await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
|
||||||
location_id: locationId,
|
location_id: locationId,
|
||||||
@@ -330,7 +333,7 @@ export default class ReservationItemService {
|
|||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
|
||||||
await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
|
await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
|
||||||
id: reservationItemId,
|
ids: reservationItemId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { humanizeAmount, MedusaError } from "medusa-core-utils"
|
import { MedusaError, humanizeAmount } from "medusa-core-utils"
|
||||||
import { updateInventoryAndReservations } from "@medusajs/medusa"
|
|
||||||
|
|
||||||
import { BaseService } from "medusa-interfaces"
|
import { BaseService } from "medusa-interfaces"
|
||||||
import Brightpearl from "../utils/brightpearl"
|
import Brightpearl from "../utils/brightpearl"
|
||||||
|
import { updateInventoryAndReservations } from "@medusajs/medusa"
|
||||||
|
|
||||||
class BrightpearlService extends BaseService {
|
class BrightpearlService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -117,7 +117,7 @@ class BrightpearlService extends BaseService {
|
|||||||
httpMethod: "POST",
|
httpMethod: "POST",
|
||||||
uriTemplate: `${this.options.backend_url}/brightpearl/inventory-update`,
|
uriTemplate: `${this.options.backend_url}/brightpearl/inventory-update`,
|
||||||
bodyTemplate:
|
bodyTemplate:
|
||||||
'{"account": "${account-code}", "lifecycle_event": "${lifecycle-event}", "resource_type": "${resource-type}", "id": "${resource-id}" }',
|
"{\"account\": \"${account-code}\", \"lifecycle_event\": \"${lifecycle-event}\", \"resource_type\": \"${resource-type}\", \"id\": \"${resource-id}\" }",
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
idSetAccepted: false,
|
idSetAccepted: false,
|
||||||
},
|
},
|
||||||
@@ -760,8 +760,14 @@ class BrightpearlService extends BaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create reservation based on reservation created event
|
||||||
|
* @param {{ ids: string[] }} eventData Event data from reservation created
|
||||||
|
*/
|
||||||
async createReservation(eventData) {
|
async createReservation(eventData) {
|
||||||
const { id } = eventData
|
const { ids } = eventData
|
||||||
|
|
||||||
|
const [id] = ids
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return
|
return
|
||||||
|
|||||||
+11
-10
@@ -1,13 +1,3 @@
|
|||||||
import { IInventoryService, InventoryItemDTO } from "@medusajs/types"
|
|
||||||
import { MedusaError } from "@medusajs/utils"
|
|
||||||
import { EntityManager } from "typeorm"
|
|
||||||
import { ulid } from "ulid"
|
|
||||||
import { ProductVariant } from "../../../../../models"
|
|
||||||
import {
|
|
||||||
ProductVariantInventoryService,
|
|
||||||
ProductVariantService,
|
|
||||||
} from "../../../../../services"
|
|
||||||
import { CreateProductVariantInput } from "../../../../../types/product-variant"
|
|
||||||
import {
|
import {
|
||||||
DistributedTransaction,
|
DistributedTransaction,
|
||||||
TransactionHandlerType,
|
TransactionHandlerType,
|
||||||
@@ -16,6 +6,17 @@ import {
|
|||||||
TransactionState,
|
TransactionState,
|
||||||
TransactionStepsDefinition,
|
TransactionStepsDefinition,
|
||||||
} from "../../../../../utils/transaction"
|
} from "../../../../../utils/transaction"
|
||||||
|
import { IInventoryService, InventoryItemDTO } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ProductVariantInventoryService,
|
||||||
|
ProductVariantService,
|
||||||
|
} from "../../../../../services"
|
||||||
|
|
||||||
|
import { CreateProductVariantInput } from "../../../../../types/product-variant"
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
import { MedusaError } from "@medusajs/utils"
|
||||||
|
import { ProductVariant } from "../../../../../models"
|
||||||
|
import { ulid } from "ulid"
|
||||||
|
|
||||||
enum actions {
|
enum actions {
|
||||||
createVariants = "createVariants",
|
createVariants = "createVariants",
|
||||||
|
|||||||
@@ -3,21 +3,22 @@ import {
|
|||||||
ICacheService,
|
ICacheService,
|
||||||
IEventBusService,
|
IEventBusService,
|
||||||
IInventoryService,
|
IInventoryService,
|
||||||
|
IStockLocationService,
|
||||||
InventoryItemDTO,
|
InventoryItemDTO,
|
||||||
InventoryLevelDTO,
|
InventoryLevelDTO,
|
||||||
IStockLocationService,
|
|
||||||
ReservationItemDTO,
|
ReservationItemDTO,
|
||||||
ReserveQuantityContext,
|
ReserveQuantityContext,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { LineItem, Product, ProductVariant } from "../models"
|
import { LineItem, Product, ProductVariant } from "../models"
|
||||||
import { isDefined, MedusaError } from "@medusajs/utils"
|
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||||
import { PricedProduct, PricedVariant } from "../types/pricing"
|
import { PricedProduct, PricedVariant } from "../types/pricing"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
|
||||||
|
|
||||||
import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item"
|
import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item"
|
||||||
import ProductVariantService from "./product-variant"
|
import ProductVariantService from "./product-variant"
|
||||||
import SalesChannelInventoryService from "./sales-channel-inventory"
|
import SalesChannelInventoryService from "./sales-channel-inventory"
|
||||||
import SalesChannelLocationService from "./sales-channel-location"
|
import SalesChannelLocationService from "./sales-channel-location"
|
||||||
|
import { TransactionBaseService } from "../interfaces"
|
||||||
|
import { getSetDifference } from "../utils/diff-set"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -253,61 +254,146 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
* @param requiredQuantity quantity of variant to attach
|
* @param requiredQuantity quantity of variant to attach
|
||||||
* @returns the variant inventory item
|
* @returns the variant inventory item
|
||||||
*/
|
*/
|
||||||
|
async attachInventoryItem(
|
||||||
|
attachments: {
|
||||||
|
variantId: string
|
||||||
|
inventoryItemId: string
|
||||||
|
requiredQuantity?: number
|
||||||
|
}[]
|
||||||
|
): Promise<ProductVariantInventoryItem[]>
|
||||||
async attachInventoryItem(
|
async attachInventoryItem(
|
||||||
variantId: string,
|
variantId: string,
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
requiredQuantity?: number
|
requiredQuantity?: number
|
||||||
): Promise<ProductVariantInventoryItem> {
|
): Promise<ProductVariantInventoryItem[]>
|
||||||
|
async attachInventoryItem(
|
||||||
|
variantIdOrAttachments:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
variantId: string
|
||||||
|
inventoryItemId: string
|
||||||
|
requiredQuantity?: number
|
||||||
|
}[],
|
||||||
|
inventoryItemId?: string,
|
||||||
|
requiredQuantity?: number
|
||||||
|
): Promise<ProductVariantInventoryItem[]> {
|
||||||
|
const data = Array.isArray(variantIdOrAttachments)
|
||||||
|
? variantIdOrAttachments
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
variantId: variantIdOrAttachments,
|
||||||
|
inventoryItemId: inventoryItemId!,
|
||||||
|
requiredQuantity,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const invalidDataEntries = data.filter(
|
||||||
|
(d) => typeof d.requiredQuantity === "number" && d.requiredQuantity < 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if (invalidDataEntries.length) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`"requiredQuantity" must be greater than 0, the following entries are invalid: ${invalidDataEntries
|
||||||
|
.map((d) => JSON.stringify(d))
|
||||||
|
.join(", ")}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify that variant exists
|
// Verify that variant exists
|
||||||
await this.productVariantService_
|
const variants = await this.productVariantService_
|
||||||
.withTransaction(this.activeManager_)
|
.withTransaction(this.activeManager_)
|
||||||
.retrieve(variantId, {
|
.list({
|
||||||
select: ["id"],
|
id: data.map((d) => d.variantId),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const foundVariantIds = new Set(variants.map((v) => v.id))
|
||||||
|
const requestedVariantIds = new Set(data.map((v) => v.variantId))
|
||||||
|
if (foundVariantIds.size !== requestedVariantIds.size) {
|
||||||
|
const difference = getSetDifference(requestedVariantIds, foundVariantIds)
|
||||||
|
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Variants not found for the following ids: ${[...difference].join(
|
||||||
|
", "
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify that item exists
|
// Verify that item exists
|
||||||
await this.inventoryService_.retrieveInventoryItem(
|
const [inventoryItems] = await this.inventoryService_.listInventoryItems(
|
||||||
inventoryItemId,
|
{
|
||||||
|
id: data.map((d) => d.inventoryItemId),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
select: ["id"],
|
select: ["id"],
|
||||||
},
|
},
|
||||||
{ transactionManager: this.activeManager_ }
|
{
|
||||||
|
transactionManager: this.activeManager_,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const foundInventoryItemIds = new Set(inventoryItems.map((v) => v.id))
|
||||||
|
const requestedInventoryItemIds = new Set(
|
||||||
|
data.map((v) => v.inventoryItemId)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (foundInventoryItemIds.size !== requestedInventoryItemIds.size) {
|
||||||
|
const difference = getSetDifference(
|
||||||
|
requestedInventoryItemIds,
|
||||||
|
foundInventoryItemIds
|
||||||
|
)
|
||||||
|
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Inventory items not found for the following ids: ${[
|
||||||
|
...difference,
|
||||||
|
].join(", ")}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const variantInventoryRepo = this.activeManager_.getRepository(
|
const variantInventoryRepo = this.activeManager_.getRepository(
|
||||||
ProductVariantInventoryItem
|
ProductVariantInventoryItem
|
||||||
)
|
)
|
||||||
|
|
||||||
const existing = await variantInventoryRepo.findOne({
|
const existingAttachments = await variantInventoryRepo.find({
|
||||||
where: {
|
where: data.map((d) => ({
|
||||||
variant_id: variantId,
|
variant_id: d.variantId,
|
||||||
inventory_item_id: inventoryItemId,
|
inventory_item_id: d.inventoryItemId,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
const existingMap: Map<string, Set<string>> = existingAttachments.reduce(
|
||||||
|
(acc, curr) => {
|
||||||
|
const existingSet = acc.get(curr.variant_id) || new Set()
|
||||||
|
existingSet.add(curr.inventory_item_id)
|
||||||
|
acc.set(curr.variant_id, existingSet)
|
||||||
|
return acc
|
||||||
},
|
},
|
||||||
})
|
new Map()
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
|
|
||||||
let quantityToStore = 1
|
|
||||||
if (typeof requiredQuantity !== "undefined") {
|
|
||||||
if (requiredQuantity < 1) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
"Quantity must be greater than 0"
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
quantityToStore = requiredQuantity
|
const toCreate = (
|
||||||
}
|
await Promise.all(
|
||||||
|
data.map(async (d) => {
|
||||||
|
if (existingMap.get(d.variantId)?.has(d.inventoryItemId)) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantInventory = variantInventoryRepo.create({
|
return variantInventoryRepo.create({
|
||||||
variant_id: variantId,
|
variant_id: d.variantId,
|
||||||
inventory_item_id: inventoryItemId,
|
inventory_item_id: d.inventoryItemId,
|
||||||
required_quantity: quantityToStore,
|
required_quantity: d.requiredQuantity ?? 1,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).filter(
|
||||||
|
(
|
||||||
|
tc: ProductVariantInventoryItem | null
|
||||||
|
): tc is ProductVariantInventoryItem => !!tc
|
||||||
|
)
|
||||||
|
|
||||||
return await variantInventoryRepo.save(variantInventory)
|
return await variantInventoryRepo.save(toCreate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -411,22 +497,22 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
locationId = locations[0].location_id
|
locationId = locations[0].location_id
|
||||||
}
|
}
|
||||||
|
|
||||||
const reservationItems = await Promise.all(
|
const reservationItems =
|
||||||
variantInventory.map(async (inventoryPart) => {
|
await this.inventoryService_.createReservationItems(
|
||||||
|
variantInventory.map((inventoryPart) => {
|
||||||
const itemQuantity = inventoryPart.required_quantity * quantity
|
const itemQuantity = inventoryPart.required_quantity * quantity
|
||||||
return await this.inventoryService_.createReservationItem(
|
|
||||||
{
|
return {
|
||||||
...toReserve,
|
...toReserve,
|
||||||
location_id: locationId as string,
|
location_id: locationId as string,
|
||||||
inventory_item_id: inventoryPart.inventory_item_id,
|
inventory_item_id: inventoryPart.inventory_item_id,
|
||||||
quantity: itemQuantity,
|
quantity: itemQuantity,
|
||||||
},
|
}
|
||||||
|
}),
|
||||||
moduleContext
|
moduleContext
|
||||||
)
|
)
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return reservationItems
|
return reservationItems.flat()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export function getSetDifference<T>(
|
||||||
|
orignalSet: Set<T>,
|
||||||
|
compareSet: Set<T>
|
||||||
|
): Set<T> {
|
||||||
|
const difference = new Set<T>()
|
||||||
|
|
||||||
|
orignalSet.forEach((element) => {
|
||||||
|
if (!compareSet.has(element)) {
|
||||||
|
difference.add(element)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return difference
|
||||||
|
}
|
||||||
@@ -268,6 +268,11 @@ export type UpdateInventoryLevelInput = {
|
|||||||
incoming_quantity?: number
|
incoming_quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BulkUpdateInventoryLevelInput = {
|
||||||
|
inventory_item_id: string
|
||||||
|
location_id: string
|
||||||
|
} & UpdateInventoryLevelInput
|
||||||
|
|
||||||
export type UpdateReservationItemInput = {
|
export type UpdateReservationItemInput = {
|
||||||
quantity?: number
|
quantity?: number
|
||||||
location_id?: string
|
location_id?: string
|
||||||
|
|||||||
@@ -56,17 +56,39 @@ export interface IInventoryService {
|
|||||||
context?: SharedContext
|
context?: SharedContext
|
||||||
): Promise<ReservationItemDTO>
|
): Promise<ReservationItemDTO>
|
||||||
|
|
||||||
// TODO make it bulk
|
createReservationItems(
|
||||||
|
input: CreateReservationItemInput[],
|
||||||
|
context?: SharedContext
|
||||||
|
): Promise<ReservationItemDTO[]>
|
||||||
|
|
||||||
createInventoryItem(
|
createInventoryItem(
|
||||||
input: CreateInventoryItemInput,
|
input: CreateInventoryItemInput,
|
||||||
context?: SharedContext
|
context?: SharedContext
|
||||||
): Promise<InventoryItemDTO>
|
): Promise<InventoryItemDTO>
|
||||||
|
|
||||||
|
createInventoryItems(
|
||||||
|
input: CreateInventoryItemInput[],
|
||||||
|
context?: SharedContext
|
||||||
|
): Promise<InventoryItemDTO[]>
|
||||||
|
|
||||||
createInventoryLevel(
|
createInventoryLevel(
|
||||||
data: CreateInventoryLevelInput,
|
data: CreateInventoryLevelInput ,
|
||||||
context?: SharedContext
|
context?: SharedContext
|
||||||
): Promise<InventoryLevelDTO>
|
): Promise<InventoryLevelDTO>
|
||||||
|
|
||||||
|
createInventoryLevels(
|
||||||
|
data: CreateInventoryLevelInput[],
|
||||||
|
context?: SharedContext
|
||||||
|
): Promise<InventoryLevelDTO[]>
|
||||||
|
|
||||||
|
updateInventoryLevels(
|
||||||
|
updates: ({
|
||||||
|
inventory_item_id: string
|
||||||
|
location_id: string
|
||||||
|
} & UpdateInventoryLevelInput)[],
|
||||||
|
context?: SharedContext
|
||||||
|
): Promise<InventoryLevelDTO[]>
|
||||||
|
|
||||||
updateInventoryLevel(
|
updateInventoryLevel(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string,
|
locationId: string,
|
||||||
@@ -98,17 +120,17 @@ export interface IInventoryService {
|
|||||||
|
|
||||||
// TODO make it bulk
|
// TODO make it bulk
|
||||||
deleteInventoryItem(
|
deleteInventoryItem(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string | string[],
|
||||||
context?: SharedContext
|
context?: SharedContext
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
deleteInventoryItemLevelByLocationId(
|
deleteInventoryItemLevelByLocationId(
|
||||||
locationId: string,
|
locationId: string | string[],
|
||||||
context?: SharedContext
|
context?: SharedContext
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
deleteReservationItemByLocationId(
|
deleteReservationItemByLocationId(
|
||||||
locationId: string,
|
locationId: string | string[],
|
||||||
context?: SharedContext
|
context?: SharedContext
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user