From c19d276458d28073a38906978fba9d11b46e5d06 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Fri, 8 Mar 2024 14:09:05 +0100 Subject: [PATCH] feat(inventory-next, types): inventory module conversion (#6596) * init * create new interface * prep integration tests * update denpencies * inventory service partial tests * finalize integration tests * add events * align events * adjust inventory level reservation levels * add test validating reserved quantity after reseration item update * fix nits * rename to inventory-next * update yarn.lock * remove changelog * remove fixtures * remove unused files * ready for review * Update packages/inventory-next/package.json Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> * pr feedback * add tests and docs for partition-array util * remote decorators from private method * fix unit tests * add migrations * add foreign keys * fix build --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --- packages/inventory-next/.gitignore | 6 + .../inventory-module-service.spec.ts | 873 ++++++++++++++ packages/inventory-next/jest.config.js | 13 + .../inventory-next/mikro-orm.config.dev.ts | 13 + packages/inventory-next/package.json | 58 + packages/inventory-next/src/index.ts | 14 + packages/inventory-next/src/joiner-config.ts | 60 + .../.snapshot-medusa-inventory.json | 570 +++++++++ .../src/migrations/Migration20240307132720.ts | 39 + packages/inventory-next/src/models/index.ts | 3 + .../src/models/inventory-item.ts | 120 ++ .../src/models/inventory-level.ts | 104 ++ .../src/models/reservation-item.ts | 107 ++ .../inventory-next/src/module-definition.ts | 44 + .../inventory-next/src/repositories/index.ts | 2 + .../src/repositories/inventory-level.ts | 72 ++ packages/inventory-next/src/schema/index.ts | 55 + .../src/services/__tests__/noop.ts | 5 + packages/inventory-next/src/services/index.ts | 2 + .../src/services/inventory-level.ts | 79 ++ .../inventory-next/src/services/inventory.ts | 1034 +++++++++++++++++ packages/inventory-next/tsconfig.json | 38 + packages/inventory-next/tsconfig.spec.json | 5 + .../src/module-test-runner.ts | 4 +- packages/types/src/inventory/bundle.ts | 2 + packages/types/src/inventory/common.ts | 26 +- packages/types/src/inventory/common/index.ts | 3 + .../src/inventory/common/inventory-item.ts | 124 ++ .../src/inventory/common/inventory-level.ts | 76 ++ .../src/inventory/common/reservation-item.ts | 107 ++ packages/types/src/inventory/index.ts | 2 + .../types/src/inventory/mutations/index.ts | 3 + .../src/inventory/mutations/inventory-item.ts | 67 ++ .../inventory/mutations/inventory-level.ts | 58 + .../inventory/mutations/reservation-item.ts | 70 ++ packages/types/src/inventory/service-next.ts | 963 +++++++++++++++ packages/utils/src/bundles.ts | 1 + .../common/__tests__/partition-array.spec.ts | 12 + packages/utils/src/common/index.ts | 1 + packages/utils/src/common/partition-array.ts | 30 + packages/utils/src/event-bus/common-events.ts | 1 + packages/utils/src/index.ts | 1 + packages/utils/src/inventory/events.ts | 14 + packages/utils/src/inventory/index.ts | 1 + .../loaders/container-loader-factory.ts | 3 +- yarn.lock | 25 + 46 files changed, 4896 insertions(+), 14 deletions(-) create mode 100644 packages/inventory-next/.gitignore create mode 100644 packages/inventory-next/integration-tests/__tests__/inventory-module-service.spec.ts create mode 100644 packages/inventory-next/jest.config.js create mode 100644 packages/inventory-next/mikro-orm.config.dev.ts create mode 100644 packages/inventory-next/package.json create mode 100644 packages/inventory-next/src/index.ts create mode 100644 packages/inventory-next/src/joiner-config.ts create mode 100644 packages/inventory-next/src/migrations/.snapshot-medusa-inventory.json create mode 100644 packages/inventory-next/src/migrations/Migration20240307132720.ts create mode 100644 packages/inventory-next/src/models/index.ts create mode 100644 packages/inventory-next/src/models/inventory-item.ts create mode 100644 packages/inventory-next/src/models/inventory-level.ts create mode 100644 packages/inventory-next/src/models/reservation-item.ts create mode 100644 packages/inventory-next/src/module-definition.ts create mode 100644 packages/inventory-next/src/repositories/index.ts create mode 100644 packages/inventory-next/src/repositories/inventory-level.ts create mode 100644 packages/inventory-next/src/schema/index.ts create mode 100644 packages/inventory-next/src/services/__tests__/noop.ts create mode 100644 packages/inventory-next/src/services/index.ts create mode 100644 packages/inventory-next/src/services/inventory-level.ts create mode 100644 packages/inventory-next/src/services/inventory.ts create mode 100644 packages/inventory-next/tsconfig.json create mode 100644 packages/inventory-next/tsconfig.spec.json create mode 100644 packages/types/src/inventory/bundle.ts create mode 100644 packages/types/src/inventory/common/index.ts create mode 100644 packages/types/src/inventory/common/inventory-item.ts create mode 100644 packages/types/src/inventory/common/inventory-level.ts create mode 100644 packages/types/src/inventory/common/reservation-item.ts create mode 100644 packages/types/src/inventory/mutations/index.ts create mode 100644 packages/types/src/inventory/mutations/inventory-item.ts create mode 100644 packages/types/src/inventory/mutations/inventory-level.ts create mode 100644 packages/types/src/inventory/mutations/reservation-item.ts create mode 100644 packages/types/src/inventory/service-next.ts create mode 100644 packages/utils/src/common/__tests__/partition-array.spec.ts create mode 100644 packages/utils/src/common/partition-array.ts create mode 100644 packages/utils/src/inventory/events.ts create mode 100644 packages/utils/src/inventory/index.ts diff --git a/packages/inventory-next/.gitignore b/packages/inventory-next/.gitignore new file mode 100644 index 0000000000..874c6c69d3 --- /dev/null +++ b/packages/inventory-next/.gitignore @@ -0,0 +1,6 @@ +/dist +node_modules +.DS_store +.env* +.env +*.sql diff --git a/packages/inventory-next/integration-tests/__tests__/inventory-module-service.spec.ts b/packages/inventory-next/integration-tests/__tests__/inventory-module-service.spec.ts new file mode 100644 index 0000000000..fa3bc5e696 --- /dev/null +++ b/packages/inventory-next/integration-tests/__tests__/inventory-module-service.spec.ts @@ -0,0 +1,873 @@ +import { IInventoryServiceNext, InventoryItemDTO } from "@medusajs/types" +import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils" + +import { Modules } from "@medusajs/modules-sdk" + +jest.setTimeout(100000) + +moduleIntegrationTestRunner({ + moduleName: Modules.INVENTORY, + resolve: "@medusajs/inventory-next", + testSuite: ({ + MikroOrmWrapper, + service, + }: SuiteOptions) => { + describe("Inventory Module Service", () => { + describe("create", () => { + it("should create an inventory item", async () => { + const data = { sku: "test-sku", origin_country: "test-country" } + const inventoryItem = await service.create(data) + + expect(inventoryItem).toEqual( + expect.objectContaining({ id: expect.any(String), ...data }) + ) + }) + + it("should create inventory items from array", async () => { + const data = [ + { sku: "test-sku", origin_country: "test-country" }, + { sku: "test-sku-1", origin_country: "test-country-1" }, + ] + const inventoryItems = await service.create(data) + + expect(inventoryItems).toEqual([ + expect.objectContaining({ id: expect.any(String), ...data[0] }), + expect.objectContaining({ id: expect.any(String), ...data[1] }), + ]) + }) + }) + + describe("createReservationItem", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 2, + }, + ]) + }) + + it("should create a reservationItem", async () => { + const data = { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 2, + } + + const reservationItem = await service.createReservationItems(data) + + expect(reservationItem).toEqual( + expect.objectContaining({ id: expect.any(String), ...data }) + ) + }) + + it("should create adjust reserved_quantity of inventory level after creation", async () => { + await service.createReservationItems({ + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 2, + }) + + const inventoryLevel = + await service.retrieveInventoryLevelByItemAndLocation( + inventoryItem.id, + "location-1" + ) + + expect(inventoryLevel.reserved_quantity).toEqual(2) + }) + + it("should create reservationItems from array", async () => { + const data = [ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + quantity: 3, + }, + ] + const reservationItems = await service.createReservationItems(data) + + expect(reservationItems).toEqual([ + expect.objectContaining({ id: expect.any(String), ...data[0] }), + expect.objectContaining({ id: expect.any(String), ...data[1] }), + ]) + }) + + it("should fail to create a reservationItem for a non-existing location", async () => { + const data = [ + { + inventory_item_id: inventoryItem.id, + location_id: "location-3", + quantity: 2, + }, + ] + + const err = await service + .createReservationItems(data) + .catch((error) => error) + + expect(err.message).toEqual( + `Item ${inventoryItem.id} is not stocked at location location-3` + ) + }) + }) + + describe("createInventoryLevel", () => { + let inventoryItem: InventoryItemDTO + + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + }) + + it("should create an inventoryLevel", async () => { + const data = { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + } + + const inventoryLevel = await service.createInventoryLevels(data) + + expect(inventoryLevel).toEqual( + expect.objectContaining({ id: expect.any(String), ...data }) + ) + }) + + it("should create inventoryLevels from array", async () => { + const data = [ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 3, + }, + ] + const inventoryLevels = await service.createInventoryLevels(data) + + expect(inventoryLevels).toEqual([ + expect.objectContaining({ id: expect.any(String), ...data[0] }), + expect.objectContaining({ id: expect.any(String), ...data[1] }), + ]) + }) + }) + + describe("update", () => { + let inventoryItem: InventoryItemDTO + + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + }) + + it("should update the inventory item", async () => { + const update = { + id: inventoryItem.id, + sku: "updated-sku", + } + const updated = await service.update(update) + + expect(updated).toEqual(expect.objectContaining(update)) + }) + + it("should update multiple inventory items", async () => { + const item2 = await service.create({ + sku: "test-sku-1", + }) + + const updates = [ + { + id: inventoryItem.id, + sku: "updated-sku", + }, + { + id: item2.id, + sku: "updated-sku-2", + }, + ] + const updated = await service.update(updates) + + expect(updated).toEqual([ + expect.objectContaining(updates[0]), + expect.objectContaining(updates[1]), + ]) + }) + }) + + describe("updateInventoryLevels", () => { + let inventoryLevel + let inventoryItem + + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + + const data = { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + } + + inventoryLevel = await service.createInventoryLevels(data) + }) + + it("should update inventory level", async () => { + const updatedLevel = await service.updateInventoryLevels({ + location_id: "location-1", + inventory_item_id: inventoryItem.id, + incoming_quantity: 4, + }) + + expect(updatedLevel.incoming_quantity).toEqual(4) + }) + + it("should fail to update inventory level for item in location that isn't stocked", async () => { + const error = await service + .updateInventoryLevels({ + inventory_item_id: inventoryItem.id, + location_id: "does-not-exist", + stocked_quantity: 10, + }) + .catch((error) => error) + + expect(error.message).toEqual( + `Item ${inventoryItem.id} is not stocked at location does-not-exist` + ) + }) + }) + + describe("updateReservationItems", () => { + let reservationItem + beforeEach(async () => { + const inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 2, + }, + ]) + + reservationItem = await service.createReservationItems({ + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 2, + }) + }) + + it("should update a reservationItem", async () => { + const update = { + id: reservationItem.id, + quantity: 5, + } + + const updated = await service.updateReservationItems(update) + + expect(updated).toEqual(expect.objectContaining(update)) + }) + + it("should adjust reserved_quantity of inventory level after updates increasing reserved quantity", async () => { + const update = { + id: reservationItem.id, + quantity: 5, + } + + await service.updateReservationItems(update) + + const inventoryLevel = + await service.retrieveInventoryLevelByItemAndLocation( + reservationItem.inventory_item_id, + "location-1" + ) + + expect(inventoryLevel.reserved_quantity).toEqual(update.quantity) + }) + + it("should adjust reserved_quantity of inventory level after updates decreasing reserved quantity", async () => { + const update = { + id: reservationItem.id, + quantity: 1, + } + + await service.updateReservationItems(update) + + const inventoryLevel = + await service.retrieveInventoryLevelByItemAndLocation( + reservationItem.inventory_item_id, + "location-1" + ) + + expect(inventoryLevel.reserved_quantity).toEqual(update.quantity) + }) + }) + + describe("deleteReservationItemsByLineItem", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 2, + }, + ]) + + await service.createReservationItems([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 2, + line_item_id: "line-item-id", + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 2, + line_item_id: "line-item-id", + }, + ]) + }) + + it("deleted reseravation items by line item", async () => { + const reservationsPreDeleted = await service.listReservationItems({ + line_item_id: "line-item-id", + }) + + expect(reservationsPreDeleted).toEqual([ + expect.objectContaining({ + location_id: "location-1", + quantity: 2, + line_item_id: "line-item-id", + }), + expect.objectContaining({ + location_id: "location-1", + quantity: 2, + line_item_id: "line-item-id", + }), + ]) + + await service.deleteReservationItemsByLineItem("line-item-id") + + const reservationsPostDeleted = await service.listReservationItems({ + line_item_id: "line-item-id", + }) + + expect(reservationsPostDeleted).toEqual([]) + }) + + it("adjusts inventory levels accordingly when removing reservations by line item", async () => { + await service.deleteReservationItemsByLineItem("line-item-id") + + const inventoryLevel = + await service.retrieveInventoryLevelByItemAndLocation( + inventoryItem.id, + "location-1" + ) + + expect(inventoryLevel).toEqual( + expect.objectContaining({ reserved_quantity: 2 }) + ) + }) + }) + + describe("deleteReservationItemByLocationId", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 2, + }, + ]) + + await service.createReservationItems([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 2, + line_item_id: "line-item-id", + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + quantity: 2, + line_item_id: "line-item-id", + }, + ]) + }) + + it("deleted reservation items by line item", async () => { + const reservationsPreDeleted = await service.listReservationItems({ + location_id: "location-1", + }) + + expect(reservationsPreDeleted).toEqual([ + expect.objectContaining({ + location_id: "location-1", + quantity: 2, + }), + expect.objectContaining({ + location_id: "location-1", + quantity: 2, + }), + ]) + + await service.deleteReservationItemByLocationId("location-1") + + const reservationsPostDeleted = await service.listReservationItems({ + location_id: "location-1", + }) + + expect(reservationsPostDeleted).toEqual([]) + }) + + it("adjusts inventory levels accordingly when removing reservations by line item", async () => { + const inventoryLevelPreDelete = + await service.retrieveInventoryLevelByItemAndLocation( + inventoryItem.id, + "location-1" + ) + + expect(inventoryLevelPreDelete).toEqual( + expect.objectContaining({ reserved_quantity: 4 }) + ) + + await service.deleteReservationItemByLocationId("location-1") + + const inventoryLevel = + await service.retrieveInventoryLevelByItemAndLocation( + inventoryItem.id, + "location-1" + ) + + expect(inventoryLevel).toEqual( + expect.objectContaining({ reserved_quantity: 0 }) + ) + }) + }) + + describe("deleteInventoryItemLevelByLocationId", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + reserved_quantity: 6, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 2, + }, + ]) + }) + + it("should remove inventory levels with given location id", async () => { + const inventoryLevelsPreDeletion = await service.listInventoryLevels( + {} + ) + + expect(inventoryLevelsPreDeletion).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + stocked_quantity: 2, + location_id: "location-1", + }), + expect.objectContaining({ + stocked_quantity: 2, + location_id: "location-2", + }), + ]) + ) + + await service.deleteInventoryItemLevelByLocationId("location-1") + + const inventoryLevelsPostDeletion = await service.listInventoryLevels( + {} + ) + + expect(inventoryLevelsPostDeletion).toEqual([ + expect.objectContaining({ + stocked_quantity: 2, + location_id: "location-2", + }), + ]) + }) + }) + + describe("deleteInventoryLevel", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + }, + ]) + }) + + it("should remove inventory levels with given location id", async () => { + const inventoryLevelsPreDeletion = await service.listInventoryLevels( + {} + ) + + expect(inventoryLevelsPreDeletion).toEqual([ + expect.objectContaining({ + stocked_quantity: 2, + location_id: "location-1", + }), + ]) + + await service.deleteInventoryLevel(inventoryItem.id, "location-1") + + const inventoryLevelsPostDeletion = await service.listInventoryLevels( + {} + ) + + expect(inventoryLevelsPostDeletion).toEqual([]) + }) + }) + + describe("adjustInventory", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + }, + ]) + }) + + it("should updated inventory level stocked_quantity by quantity", async () => { + const updatedLevel = await service.adjustInventory( + inventoryItem.id, + "location-1", + 2 + ) + expect(updatedLevel.stocked_quantity).toEqual(4) + }) + + it("should updated inventory level stocked_quantity by negative quantity", async () => { + const updatedLevel = await service.adjustInventory( + inventoryItem.id, + "location-1", + -1 + ) + expect(updatedLevel.stocked_quantity).toEqual(1!) + }) + }) + + describe("retrieveInventoryLevelByItemAndLocation", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + const inventoryItem1 = await service.create({ + sku: "test-sku-1", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 3, + }, + { + inventory_item_id: inventoryItem1.id, + location_id: "location-1", + stocked_quantity: 3, + }, + ]) + }) + + it("should retrieve inventory level with provided location_id and inventory_item", async () => { + const level = await service.retrieveInventoryLevelByItemAndLocation( + inventoryItem.id, + "location-1" + ) + expect(level.stocked_quantity).toEqual(2) + }) + }) + + describe("retrieveAvailableQuantity", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + const inventoryItem1 = await service.create({ + sku: "test-sku-1", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 4, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 4, + reserved_quantity: 2, + }, + { + inventory_item_id: inventoryItem1.id, + location_id: "location-1", + stocked_quantity: 3, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-3", + stocked_quantity: 3, + }, + ]) + }) + + it("should calculate current stocked quantity across locations", async () => { + const level = await service.retrieveAvailableQuantity( + inventoryItem.id, + ["location-1", "location-2"] + ) + expect(level).toEqual(6) + }) + }) + + describe("retrieveStockedQuantity", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + const inventoryItem1 = await service.create({ + sku: "test-sku-1", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 4, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 4, + reserved_quantity: 2, + }, + { + inventory_item_id: inventoryItem1.id, + location_id: "location-1", + stocked_quantity: 3, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-3", + stocked_quantity: 3, + }, + ]) + }) + + it("retrieves stocked location", async () => { + const stockedQuantity = await service.retrieveStockedQuantity( + inventoryItem.id, + ["location-1", "location-2"] + ) + + expect(stockedQuantity).toEqual(8) + }) + }) + describe("retrieveReservedQuantity", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + const inventoryItem1 = await service.create({ + sku: "test-sku-1", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 4, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 4, + reserved_quantity: 2, + }, + { + inventory_item_id: inventoryItem1.id, + location_id: "location-1", + stocked_quantity: 3, + reserved_quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-3", + stocked_quantity: 3, + reserved_quantity: 2, + }, + ]) + }) + + it("retrieves reserved quantity", async () => { + const reservedQuantity = await service.retrieveReservedQuantity( + inventoryItem.id, + ["location-1", "location-2"] + ) + + expect(reservedQuantity).toEqual(2) + }) + }) + + describe("confirmInventory", () => { + let inventoryItem: InventoryItemDTO + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "test-sku", + origin_country: "test-country", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 4, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + stocked_quantity: 4, + reserved_quantity: 2, + }, + ]) + }) + + it("should return true if quantity is less than or equal to available quantity", async () => { + const reservedQuantity = await service.confirmInventory( + inventoryItem.id, + "location-1", + 2 + ) + + expect(reservedQuantity).toBeTruthy() + }) + + it("should return true if quantity is more than available quantity", async () => { + const reservedQuantity = await service.confirmInventory( + inventoryItem.id, + "location-1", + 3 + ) + + expect(reservedQuantity).toBeTruthy() + }) + }) + }) + }, +}) diff --git a/packages/inventory-next/jest.config.js b/packages/inventory-next/jest.config.js new file mode 100644 index 0000000000..2fd636dce6 --- /dev/null +++ b/packages/inventory-next/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + transform: { + "^.+\\.[jt]s?$": [ + "ts-jest", + { + tsConfig: "tsconfig.json", + isolatedModules: true, + }, + ], + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `ts`], +} diff --git a/packages/inventory-next/mikro-orm.config.dev.ts b/packages/inventory-next/mikro-orm.config.dev.ts new file mode 100644 index 0000000000..89f3871b23 --- /dev/null +++ b/packages/inventory-next/mikro-orm.config.dev.ts @@ -0,0 +1,13 @@ +import * as entities from "./src/models" + +import { TSMigrationGenerator } from "@medusajs/utils" + +module.exports = { + entities: Object.values(entities), + schema: "public", + clientUrl: "postgres://postgres@localhost/medusa-inventory", + type: "postgresql", + migrations: { + generator: TSMigrationGenerator, + }, +} diff --git a/packages/inventory-next/package.json b/packages/inventory-next/package.json new file mode 100644 index 0000000000..e95ae50881 --- /dev/null +++ b/packages/inventory-next/package.json @@ -0,0 +1,58 @@ +{ + "name": "@medusajs/inventory-next", + "version": "0.0.1", + "description": "Inventory Module for Medusa", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/inventory-next" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "engines": { + "node": ">=16" + }, + "author": "Medusa", + "license": "MIT", + "devDependencies": { + "@medusajs/types": "^1.11.12", + "@mikro-orm/cli": "5.9.7", + "cross-env": "^5.2.1", + "jest": "^29.6.3", + "medusa-test-utils": "^1.1.40", + "rimraf": "^5.0.1", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "tsc-alias": "^1.8.6", + "typescript": "^5.1.6" + }, + "dependencies": { + "@medusajs/modules-sdk": "^1.12.4", + "@medusajs/types": "^1.11.8", + "@medusajs/utils": "^1.11.1", + "@mikro-orm/core": "5.9.7", + "@mikro-orm/migrations": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", + "awilix": "^8.0.0", + "dotenv": "16.4.5", + "knex": "2.4.2" + }, + "scripts": { + "watch": "tsc --build --watch", + "watch:test": "tsc --build tsconfig.spec.json --watch", + "prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json", + "build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json", + "test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts", + "test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.spec.ts", + "migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate", + "migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial -n InitialSetupMigration", + "migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create", + "migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up", + "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" + } +} diff --git a/packages/inventory-next/src/index.ts b/packages/inventory-next/src/index.ts new file mode 100644 index 0000000000..eda56a705e --- /dev/null +++ b/packages/inventory-next/src/index.ts @@ -0,0 +1,14 @@ +import { Modules, initializeFactory } from "@medusajs/modules-sdk" + +import { moduleDefinition } from "./module-definition" + +export * from "./models" +export * from "./services" + +export const initialize = initializeFactory({ + moduleName: Modules.INVENTORY, + moduleDefinition, +}) +export const runMigrations = moduleDefinition.runMigrations +export const revertMigration = moduleDefinition.revertMigration +export default moduleDefinition diff --git a/packages/inventory-next/src/joiner-config.ts b/packages/inventory-next/src/joiner-config.ts new file mode 100644 index 0000000000..14a0fa53ec --- /dev/null +++ b/packages/inventory-next/src/joiner-config.ts @@ -0,0 +1,60 @@ +import { InventoryItem, InventoryLevel, ReservationItem } from "./models" + +import { MapToConfig } from "@medusajs/utils" +import { ModuleJoinerConfig } from "@medusajs/types" +import { Modules } from "@medusajs/modules-sdk" +import moduleSchema from "./schema" + +export const LinkableKeys = { + inventory_item_id: InventoryItem.name, + inventory_level_id: InventoryLevel.name, + reservation_item_id: ReservationItem.name, +} + +const entityLinkableKeysMap: MapToConfig = {} +Object.entries(LinkableKeys).forEach(([key, value]) => { + entityLinkableKeysMap[value] ??= [] + entityLinkableKeysMap[value].push({ + mapTo: key, + valueFrom: key.split("_").pop()!, + }) +}) +export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap + +export const joinerConfig: ModuleJoinerConfig = { + serviceName: Modules.INVENTORY, + primaryKeys: ["id"], + linkableKeys: { + inventory_item_id: InventoryItem.name, + inventory_level_id: InventoryLevel.name, + reservation_item_id: ReservationItem.name, + }, + schema: moduleSchema, + alias: [ + { + name: ["inventory_items", "inventory"], + args: { + entity: "InventoryItem", + }, + }, + { + name: ["inventory_level", "inventory_levels"], + args: { + entity: "InventoryLevel", + methodSuffix: "InventoryLevels", + }, + }, + { + name: [ + "reservation", + "reservations", + "reservation_item", + "reservation_items", + ], + args: { + entity: "ReservationItem", + methodSuffix: "ReservationItems", + }, + }, + ], +} diff --git a/packages/inventory-next/src/migrations/.snapshot-medusa-inventory.json b/packages/inventory-next/src/migrations/.snapshot-medusa-inventory.json new file mode 100644 index 0000000000..8034b02003 --- /dev/null +++ b/packages/inventory-next/src/migrations/.snapshot-medusa-inventory.json @@ -0,0 +1,570 @@ +{ + "namespaces": [ + "public" + ], + "name": "public", + "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "sku": { + "name": "sku", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "origin_country": { + "name": "origin_country", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "hs_code": { + "name": "hs_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "mid_code": { + "name": "mid_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "material": { + "name": "material", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "weight": { + "name": "weight", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "integer" + }, + "length": { + "name": "length", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "integer" + }, + "height": { + "name": "height", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "integer" + }, + "width": { + "name": "width", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "integer" + }, + "requires_shipping": { + "name": "requires_shipping", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "true", + "mappedType": "boolean" + }, + "description": { + "name": "description", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "title": { + "name": "title", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "thumbnail": { + "name": "thumbnail", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + } + }, + "name": "inventory_item", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_inventory_item_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_item_deleted_at\" ON \"inventory_item\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "IDX_inventory_item_sku_unique", + "columnNames": [ + "sku" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_inventory_item_sku_unique\" ON \"inventory_item\" (sku)" + }, + { + "keyName": "inventory_item_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "inventory_item_id": { + "name": "inventory_item_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "location_id": { + "name": "location_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "stocked_quantity": { + "name": "stocked_quantity", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "integer" + }, + "reserved_quantity": { + "name": "reserved_quantity", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "integer" + }, + "incoming_quantity": { + "name": "incoming_quantity", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "integer" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + } + }, + "name": "inventory_level", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_inventory_level_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_deleted_at\" ON \"inventory_level\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "IDX_inventory_level_inventory_item_id", + "columnNames": [ + "inventory_item_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_inventory_item_id\" ON \"inventory_level\" (inventory_item_id)" + }, + { + "keyName": "IDX_inventory_level_location_id", + "columnNames": [ + "location_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_location_id\" ON \"inventory_level\" (location_id)" + }, + { + "keyName": "IDX_inventory_level_location_id", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_location_id\" ON \"inventory_level\" (location_id)" + }, + { + "keyName": "inventory_level_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "inventory_level_inventory_item_id_foreign": { + "constraintName": "inventory_level_inventory_item_id_foreign", + "columnNames": [ + "inventory_item_id" + ], + "localTableName": "public.inventory_level", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.inventory_item", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "line_item_id": { + "name": "line_item_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "location_id": { + "name": "location_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "quantity": { + "name": "quantity", + "type": "integer", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "external_id": { + "name": "external_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "description": { + "name": "description", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "created_by": { + "name": "created_by", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "inventory_item_id": { + "name": "inventory_item_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "reservation_item", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_reservation_item_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_deleted_at\" ON \"reservation_item\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "IDX_reservation_item_line_item_id", + "columnNames": [ + "line_item_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_line_item_id\" ON \"reservation_item\" (line_item_id)" + }, + { + "keyName": "IDX_reservation_item_location_id", + "columnNames": [ + "location_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_location_id\" ON \"reservation_item\" (location_id)" + }, + { + "keyName": "IDX_reservation_item_inventory_item_id", + "columnNames": [ + "inventory_item_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_inventory_item_id\" ON \"reservation_item\" (inventory_item_id)" + }, + { + "keyName": "reservation_item_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "reservation_item_inventory_item_id_foreign": { + "constraintName": "reservation_item_inventory_item_id_foreign", + "columnNames": [ + "inventory_item_id" + ], + "localTableName": "public.reservation_item", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.inventory_item", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + } + ] +} diff --git a/packages/inventory-next/src/migrations/Migration20240307132720.ts b/packages/inventory-next/src/migrations/Migration20240307132720.ts new file mode 100644 index 0000000000..35f4c68550 --- /dev/null +++ b/packages/inventory-next/src/migrations/Migration20240307132720.ts @@ -0,0 +1,39 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240307132720 extends Migration { + + async up(): Promise { + this.addSql('create table if not exists "inventory_item" ("id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "sku" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "weight" int null, "length" int null, "height" int null, "width" int null, "requires_shipping" boolean not null default true, "description" text null, "title" text null, "thumbnail" text null, "metadata" jsonb null, constraint "inventory_item_pkey" primary key ("id"));'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_inventory_item_deleted_at" ON "inventory_item" (deleted_at) WHERE deleted_at IS NOT NULL;'); + this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_inventory_item_sku_unique" ON "inventory_item" (sku);'); + + this.addSql('create table if not exists "inventory_level" ("id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "inventory_item_id" text not null, "location_id" text not null, "stocked_quantity" int not null default 0, "reserved_quantity" int not null default 0, "incoming_quantity" int not null default 0, "metadata" jsonb null, constraint "inventory_level_pkey" primary key ("id"));'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_inventory_level_deleted_at" ON "inventory_level" (deleted_at) WHERE deleted_at IS NOT NULL;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_inventory_level_inventory_item_id" ON "inventory_level" (inventory_item_id);'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_inventory_level_location_id" ON "inventory_level" (location_id);'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_inventory_level_location_id" ON "inventory_level" (location_id);'); + + this.addSql('create table if not exists "reservation_item" ("id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "line_item_id" text null, "location_id" text not null, "quantity" integer not null, "external_id" text null, "description" text null, "created_by" text null, "metadata" jsonb null, "inventory_item_id" text not null, constraint "reservation_item_pkey" primary key ("id"));'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_reservation_item_deleted_at" ON "reservation_item" (deleted_at) WHERE deleted_at IS NOT NULL;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_reservation_item_line_item_id" ON "reservation_item" (line_item_id);'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_reservation_item_location_id" ON "reservation_item" (location_id);'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_reservation_item_inventory_item_id" ON "reservation_item" (inventory_item_id);'); + + this.addSql('alter table if exists "inventory_level" add constraint "inventory_level_inventory_item_id_foreign" foreign key ("inventory_item_id") references "inventory_item" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table if exists "reservation_item" add constraint "reservation_item_inventory_item_id_foreign" foreign key ("inventory_item_id") references "inventory_item" ("id") on update cascade on delete cascade;'); + } + + async down(): Promise { + this.addSql('alter table if exists "inventory_level" drop constraint if exists "inventory_level_inventory_item_id_foreign";'); + + this.addSql('alter table if exists "reservation_item" drop constraint if exists "reservation_item_inventory_item_id_foreign";'); + + this.addSql('drop table if exists "inventory_item" cascade;'); + + this.addSql('drop table if exists "inventory_level" cascade;'); + + this.addSql('drop table if exists "reservation_item" cascade;'); + } + +} diff --git a/packages/inventory-next/src/models/index.ts b/packages/inventory-next/src/models/index.ts new file mode 100644 index 0000000000..8a77e08089 --- /dev/null +++ b/packages/inventory-next/src/models/index.ts @@ -0,0 +1,3 @@ +export * from "./reservation-item" +export * from "./inventory-item" +export * from "./inventory-level" diff --git a/packages/inventory-next/src/models/inventory-item.ts b/packages/inventory-next/src/models/inventory-item.ts new file mode 100644 index 0000000000..74477a0dcb --- /dev/null +++ b/packages/inventory-next/src/models/inventory-item.ts @@ -0,0 +1,120 @@ +import { + BeforeCreate, + Collection, + Entity, + Filter, + OnInit, + OneToMany, + OptionalProps, + PrimaryKey, + Property, +} from "@mikro-orm/core" +import { + DALUtils, + createPsqlIndexStatementHelper, + generateEntityId, +} from "@medusajs/utils" + +import { DAL } from "@medusajs/types" +import { InventoryLevel } from "./inventory-level" + +const InventoryItemDeletedAtIndex = createPsqlIndexStatementHelper({ + tableName: "inventory_item", + columns: "deleted_at", + where: "deleted_at IS NOT NULL", +}) + +const InventoryItemSkuIndex = createPsqlIndexStatementHelper({ + tableName: "inventory_item", + columns: "sku", + unique: true, +}) + +type InventoryItemOptionalProps = DAL.SoftDeletableEntityDateColumns + +@Entity() +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) +export class InventoryItem { + [OptionalProps]: InventoryItemOptionalProps + + @PrimaryKey({ columnType: "text" }) + id: string + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @InventoryItemDeletedAtIndex.MikroORMIndex() + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null = null + + @InventoryItemSkuIndex.MikroORMIndex() + @Property({ columnType: "text", nullable: true }) + sku: string | null = null + + @Property({ columnType: "text", nullable: true }) + origin_country: string | null = null + + @Property({ columnType: "text", nullable: true }) + hs_code: string | null = null + + @Property({ columnType: "text", nullable: true }) + mid_code: string | null = null + + @Property({ columnType: "text", nullable: true }) + material: string | null = null + + @Property({ type: "int", nullable: true }) + weight: number | null = null + + @Property({ type: "int", nullable: true }) + length: number | null = null + + @Property({ type: "int", nullable: true }) + height: number | null = null + + @Property({ type: "int", nullable: true }) + width: number | null = null + + @Property({ columnType: "boolean" }) + requires_shipping: boolean = true + + @Property({ columnType: "text", nullable: true }) + description: string | null = null + + @Property({ columnType: "text", nullable: true }) + title: string | null = null + + @Property({ columnType: "text", nullable: true }) + thumbnail: string | null = null + + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + + @OneToMany( + () => InventoryLevel, + (inventoryLevel) => inventoryLevel.inventory_item + ) + inventory_levels = new Collection(this) + + @BeforeCreate() + private beforeCreate(): void { + this.id = generateEntityId(this.id, "iitem") + } + + @OnInit() + private onInit(): void { + this.id = generateEntityId(this.id, "iitem") + } +} diff --git a/packages/inventory-next/src/models/inventory-level.ts b/packages/inventory-next/src/models/inventory-level.ts new file mode 100644 index 0000000000..b60787dfb3 --- /dev/null +++ b/packages/inventory-next/src/models/inventory-level.ts @@ -0,0 +1,104 @@ +import { + BeforeCreate, + Entity, + Filter, + ManyToOne, + OnInit, + PrimaryKey, + Property, +} from "@mikro-orm/core" + +import { DALUtils } from "@medusajs/utils" +import { InventoryItem } from "./inventory-item" +import { createPsqlIndexStatementHelper } from "@medusajs/utils" +import { generateEntityId } from "@medusajs/utils" + +const InventoryLevelDeletedAtIndex = createPsqlIndexStatementHelper({ + tableName: "inventory_level", + columns: "deleted_at", + where: "deleted_at IS NOT NULL", +}) + +const InventoryLevelInventoryItemIdIndex = createPsqlIndexStatementHelper({ + tableName: "inventory_level", + columns: "inventory_item_id", +}) + +const InventoryLevelLocationIdIndex = createPsqlIndexStatementHelper({ + tableName: "inventory_level", + columns: "location_id", +}) + +const InventoryLevelLocationIdInventoryItemIdIndex = + createPsqlIndexStatementHelper({ + tableName: "inventory_level", + columns: "location_id", + }) + +@Entity() +@InventoryLevelLocationIdInventoryItemIdIndex.MikroORMIndex() +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) +export class InventoryLevel { + @PrimaryKey({ columnType: "text" }) + id: string + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @InventoryLevelDeletedAtIndex.MikroORMIndex() + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null = null + + @ManyToOne(() => InventoryItem, { + fieldName: "inventory_item_id", + type: "text", + mapToPk: true, + onDelete: "cascade", + }) + @InventoryLevelInventoryItemIdIndex.MikroORMIndex() + inventory_item_id: string + + @InventoryLevelLocationIdIndex.MikroORMIndex() + @Property({ type: "text" }) + location_id: string + + @Property({ type: "int" }) + stocked_quantity: number = 0 + + @Property({ type: "int" }) + reserved_quantity: number = 0 + + @Property({ type: "int" }) + incoming_quantity: number = 0 + + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null + + @ManyToOne(() => InventoryItem, { + persist: false, + }) + inventory_item: InventoryItem + + @BeforeCreate() + private beforeCreate(): void { + this.id = generateEntityId(this.id, "ilev") + this.inventory_item_id ??= this.inventory_item?.id + } + + @OnInit() + private onInit(): void { + this.id = generateEntityId(this.id, "ilev") + } +} diff --git a/packages/inventory-next/src/models/reservation-item.ts b/packages/inventory-next/src/models/reservation-item.ts new file mode 100644 index 0000000000..e86d41e1a3 --- /dev/null +++ b/packages/inventory-next/src/models/reservation-item.ts @@ -0,0 +1,107 @@ +import { + BeforeCreate, + Entity, + Filter, + ManyToOne, + OnInit, + PrimaryKey, + Property, +} from "@mikro-orm/core" + +import { DALUtils } from "@medusajs/utils" +import { InventoryItem } from "./inventory-item" +import { createPsqlIndexStatementHelper } from "@medusajs/utils" +import { generateEntityId } from "@medusajs/utils" + +const ReservationItemDeletedAtIndex = createPsqlIndexStatementHelper({ + tableName: "reservation_item", + columns: "deleted_at", + where: "deleted_at IS NOT NULL", +}) +const ReservationItemLineItemIdIndex = createPsqlIndexStatementHelper({ + tableName: "reservation_item", + columns: "line_item_id", +}) + +const ReservationItemInventoryItemIdIndex = createPsqlIndexStatementHelper({ + tableName: "reservation_item", + columns: "inventory_item_id", +}) + +const ReservationItemLocationIdIndex = createPsqlIndexStatementHelper({ + tableName: "reservation_item", + columns: "location_id", +}) + +@Entity() +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) +export class ReservationItem { + @PrimaryKey({ columnType: "text" }) + id: string + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @ReservationItemDeletedAtIndex.MikroORMIndex() + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null = null + + @ReservationItemLineItemIdIndex.MikroORMIndex() + @Property({ type: "text", nullable: true }) + line_item_id: string | null = null + + @ReservationItemLocationIdIndex.MikroORMIndex() + @Property({ type: "text" }) + location_id: string + + @Property({ columnType: "integer" }) + quantity: number + + @Property({ type: "text", nullable: true }) + external_id: string | null = null + + @Property({ type: "text", nullable: true }) + description: string | null = null + + @Property({ type: "text", nullable: true }) + created_by: string | null = null + + @Property({ type: "jsonb", nullable: true }) + metadata: Record | null = null + + @ReservationItemInventoryItemIdIndex.MikroORMIndex() + @ManyToOne(() => InventoryItem, { + fieldName: "inventory_item_id", + type: "text", + mapToPk: true, + onDelete: "cascade", + }) + inventory_item_id: string + + @ManyToOne(() => InventoryItem, { + persist: false, + }) + inventory_item: InventoryItem + + @BeforeCreate() + private beforeCreate(): void { + this.id = generateEntityId(this.id, "ilev") + } + + @OnInit() + private onInit(): void { + this.id = generateEntityId(this.id, "ilev") + } +} diff --git a/packages/inventory-next/src/module-definition.ts b/packages/inventory-next/src/module-definition.ts new file mode 100644 index 0000000000..18fbbf404a --- /dev/null +++ b/packages/inventory-next/src/module-definition.ts @@ -0,0 +1,44 @@ +import * as InventoryModels from "@models" +import * as InventoryRepositories from "@repositories" +import * as InventoryServices from "@services" + +import InventoryService from "./services/inventory" +import { ModuleExports } from "@medusajs/types" +import { Modules } from "@medusajs/modules-sdk" +import { ModulesSdkUtils } from "@medusajs/utils" + +const migrationScriptOptions = { + moduleName: Modules.INVENTORY, + models: InventoryModels, + pathToMigrations: __dirname + "/migrations", +} + +const runMigrations = ModulesSdkUtils.buildMigrationScript( + migrationScriptOptions +) + +const revertMigration = ModulesSdkUtils.buildRevertMigrationScript( + migrationScriptOptions +) + +const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({ + moduleModels: InventoryModels, + moduleRepositories: InventoryRepositories, + moduleServices: InventoryServices, +}) + +const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({ + moduleName: Modules.INVENTORY, + moduleModels: Object.values(InventoryModels), + migrationsPath: __dirname + "/migrations", +}) + +const service = InventoryService +const loaders = [containerLoader, connectionLoader] + +export const moduleDefinition: ModuleExports = { + service, + loaders, + revertMigration, + runMigrations, +} diff --git a/packages/inventory-next/src/repositories/index.ts b/packages/inventory-next/src/repositories/index.ts new file mode 100644 index 0000000000..dc31fdaa30 --- /dev/null +++ b/packages/inventory-next/src/repositories/index.ts @@ -0,0 +1,2 @@ +export * from "./inventory-level" +export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" diff --git a/packages/inventory-next/src/repositories/inventory-level.ts b/packages/inventory-next/src/repositories/inventory-level.ts new file mode 100644 index 0000000000..e92437b899 --- /dev/null +++ b/packages/inventory-next/src/repositories/inventory-level.ts @@ -0,0 +1,72 @@ +import { Context } from "@medusajs/types" +import { InventoryLevel } from "@models" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { mikroOrmBaseRepositoryFactory } from "@medusajs/utils" + +export class InventoryLevelRepository extends mikroOrmBaseRepositoryFactory( + InventoryLevel +) { + async getReservedQuantity( + inventoryItemId: string, + locationIds: string[], + context: Context = {} + ): Promise { + const manager = super.getActiveManager(context) + + const [result] = (await manager + .getKnex()({ il: "inventory_level" }) + .sum("reserved_quantity") + .whereIn("location_id", locationIds) + .andWhere("inventory_item_id", inventoryItemId)) as { + sum: string + }[] + + return parseInt(result.sum) + } + + async getAvailableQuantity( + inventoryItemId: string, + locationIds: string[], + context: Context = {} + ): Promise { + const knex = super.getActiveManager(context).getKnex() + + const [result] = (await knex({ + il: "inventory_level", + }) + .sum({ + stocked_quantity: "stocked_quantity", + reserved_quantity: "reserved_quantity", + }) + .whereIn("location_id", locationIds) + .andWhere("inventory_item_id", inventoryItemId)) as { + reserved_quantity: string + stocked_quantity: string + }[] + + return ( + parseInt(result.stocked_quantity) - parseInt(result.reserved_quantity) + ) + } + + async getStockedQuantity( + inventoryItemId: string, + locationIds: string[], + context: Context = {} + ): Promise { + const knex = super.getActiveManager(context).getKnex() + + const [result] = (await knex({ + il: "inventory_level", + }) + .sum({ + stocked_quantity: "stocked_quantity", + }) + .whereIn("location_id", locationIds) + .andWhere("inventory_item_id", inventoryItemId)) as { + stocked_quantity: string + }[] + + return parseInt(result.stocked_quantity) + } +} diff --git a/packages/inventory-next/src/schema/index.ts b/packages/inventory-next/src/schema/index.ts new file mode 100644 index 0000000000..859d6efca7 --- /dev/null +++ b/packages/inventory-next/src/schema/index.ts @@ -0,0 +1,55 @@ +export default ` +scalar DateTime +scalar JSON + +type InventoryItem { + id: ID! + created_at: DateTime! + updated_at: DateTime! + deleted_at: DateTime + sku: String + origin_country: String + hs_code: String + mid_code: String + material: String + weight: Int + length: Int + height: Int + width: Int + requires_shipping: Boolean! + description: String + title: String + thumbnail: String + metadata: JSON + + inventory_levels: [InventoryLevel] +} + +type InventoryLevel { + id: ID! + created_at: DateTime! + updated_at: DateTime! + deleted_at: DateTime + inventory_item_id: String! + location_id: String! + stocked_quantity: Int! + reserved_quantity: Int! + incoming_quantity: Int! + metadata: JSON +} + +type ReservationItem { + id: ID! + created_at: DateTime! + updated_at: DateTime! + deleted_at: DateTime + line_item_id: String + inventory_item_id: String! + location_id: String! + quantity: Int! + external_id: String + description: String + created_by: String + metadata: JSON +} +` diff --git a/packages/inventory-next/src/services/__tests__/noop.ts b/packages/inventory-next/src/services/__tests__/noop.ts new file mode 100644 index 0000000000..333c84c1dd --- /dev/null +++ b/packages/inventory-next/src/services/__tests__/noop.ts @@ -0,0 +1,5 @@ +describe("noop", function () { + it("should run", function () { + expect(true).toBe(true) + }) +}) diff --git a/packages/inventory-next/src/services/index.ts b/packages/inventory-next/src/services/index.ts new file mode 100644 index 0000000000..3cf8942bef --- /dev/null +++ b/packages/inventory-next/src/services/index.ts @@ -0,0 +1,2 @@ +export { default as InventoryModuleService } from "./inventory" +export { default as InventoryLevelService } from "./inventory-level" diff --git a/packages/inventory-next/src/services/inventory-level.ts b/packages/inventory-next/src/services/inventory-level.ts new file mode 100644 index 0000000000..01360fafa5 --- /dev/null +++ b/packages/inventory-next/src/services/inventory-level.ts @@ -0,0 +1,79 @@ +import { + Context, + CreateInventoryLevelInput, + DAL, + SharedContext, +} from "@medusajs/types" +import { + InjectTransactionManager, + MedusaContext, + ModulesSdkUtils, +} from "@medusajs/utils" + +import { InventoryLevel } from "../models/inventory-level" +import { InventoryLevelRepository } from "@repositories" + +type InjectedDependencies = { + inventoryLevelRepository: InventoryLevelRepository +} + +export default class InventoryLevelService< + TEntity extends InventoryLevel = InventoryLevel +> extends ModulesSdkUtils.internalModuleServiceFactory( + InventoryLevel +) { + protected readonly inventoryLevelRepository: InventoryLevelRepository + + constructor(container: InjectedDependencies) { + super(container) + this.inventoryLevelRepository = container.inventoryLevelRepository + } + + async retrieveStockedQuantity( + inventoryItemId: string, + locationIds: string[] | string, + context: Context = {} + ): Promise { + const locationIdArray = Array.isArray(locationIds) + ? locationIds + : [locationIds] + + return await this.inventoryLevelRepository.getStockedQuantity( + inventoryItemId, + locationIdArray, + context + ) + } + + async getAvailableQuantity( + inventoryItemId: string, + locationIds: string[] | string, + context: Context = {} + ): Promise { + const locationIdArray = Array.isArray(locationIds) + ? locationIds + : [locationIds] + + return await this.inventoryLevelRepository.getAvailableQuantity( + inventoryItemId, + locationIdArray, + context + ) + } + + async getReservedQuantity( + inventoryItemId: string, + locationIds: string[] | string, + context: Context = {} + ) { + if (!Array.isArray(locationIds)) { + locationIds = [locationIds] + } + + return await this.inventoryLevelRepository.getReservedQuantity( + inventoryItemId, + locationIds, + context + ) + } +} diff --git a/packages/inventory-next/src/services/inventory.ts b/packages/inventory-next/src/services/inventory.ts new file mode 100644 index 0000000000..14fce92648 --- /dev/null +++ b/packages/inventory-next/src/services/inventory.ts @@ -0,0 +1,1034 @@ +import { InternalModuleDeclaration } from "@medusajs/modules-sdk" +import { + Context, + IInventoryServiceNext, + InventoryTypes, + ModuleJoinerConfig, + ModulesSdkTypes, + InventoryNext, + ReservationItemDTO, +} from "@medusajs/types" +import { + EmitEvents, + MedusaContext, + MedusaError, + ModulesSdkUtils, +} from "@medusajs/utils" +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" +import { InventoryItem, InventoryLevel, ReservationItem } from "@models" +import { DAL } from "@medusajs/types" +import { InjectTransactionManager } from "@medusajs/utils" +import { InjectManager } from "@medusajs/utils" +import InventoryLevelService from "./inventory-level" +import { partitionArray } from "@medusajs/utils" +import { InventoryEvents } from "@medusajs/utils" +import { CommonEvents } from "@medusajs/utils" +import { isDefined } from "@medusajs/utils" + +type InjectedDependencies = { + baseRepository: DAL.RepositoryService + inventoryItemService: ModulesSdkTypes.InternalModuleService + inventoryLevelService: InventoryLevelService + reservationItemService: ModulesSdkTypes.InternalModuleService +} + +const generateMethodForModels = [InventoryItem, InventoryLevel, ReservationItem] + +export default class InventoryModuleService< + TInventoryItem extends InventoryItem = InventoryItem, + TInventoryLevel extends InventoryLevel = InventoryLevel, + TReservationItem extends ReservationItem = ReservationItem + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + InventoryNext.InventoryItemDTO, + { + InventoryItem: { + dto: InventoryNext.InventoryItemDTO + } + InventoryLevel: { + dto: InventoryNext.InventoryLevelDTO + } + ReservationItem: { + dto: InventoryNext.ReservationItemDTO + } + } + >(InventoryItem, generateMethodForModels, entityNameToLinkableKeysMap) + implements IInventoryServiceNext +{ + protected baseRepository_: DAL.RepositoryService + + protected readonly inventoryItemService_: ModulesSdkTypes.InternalModuleService + protected readonly reservationItemService_: ModulesSdkTypes.InternalModuleService + protected readonly inventoryLevelService_: InventoryLevelService + + constructor( + { + baseRepository, + inventoryItemService, + inventoryLevelService, + reservationItemService, + }: InjectedDependencies, + protected readonly moduleDeclaration?: InternalModuleDeclaration + ) { + // @ts-ignore + // eslint-disable-next-line prefer-rest-params + super(...arguments) + + this.baseRepository_ = baseRepository + this.inventoryItemService_ = inventoryItemService + this.inventoryLevelService_ = inventoryLevelService + this.reservationItemService_ = reservationItemService + } + + __joinerConfig(): ModuleJoinerConfig { + return joinerConfig + } + + private async ensureInventoryLevels( + data: ( + | { location_id: string; inventory_item_id: string } + | { id: string } + )[], + context: Context + ): Promise { + const [idData, itemLocationData] = partitionArray( + data, + ({ id }) => !!id + ) as [ + { id: string }[], + { location_id: string; inventory_item_id: string }[] + ] + + const inventoryLevels = await this.inventoryLevelService_.list( + { + $or: [ + { id: idData.filter(({ id }) => !!id).map((e) => e.id) }, + ...itemLocationData, + ], + }, + {}, + context + ) + + const inventoryLevelIdMap: Map = + new Map(inventoryLevels.map((level) => [level.id, level])) + + const inventoryLevelItemLocationMap: Map< + string, + Map + > = 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) => { + if ("id" in i) { + return !inventoryLevelIdMap.has(i.id) + } + return !inventoryLevelItemLocationMap + .get(i.inventory_item_id) + ?.has(i.location_id) + }) + + if (missing.length) { + const error = missing + .map((missing) => { + if ("id" in missing) { + return `Inventory level with id ${missing.id} does not exist` + } + 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 + } + + async createReservationItems( + input: InventoryNext.CreateReservationItemInput[], + context?: Context + ): Promise + async createReservationItems( + input: InventoryNext.CreateReservationItemInput, + context?: Context + ): Promise + + @InjectManager("baseRepository_") + @EmitEvents() + async createReservationItems( + input: + | InventoryNext.CreateReservationItemInput[] + | InventoryNext.CreateReservationItemInput, + @MedusaContext() context: Context = {} + ): Promise< + InventoryNext.ReservationItemDTO[] | InventoryNext.ReservationItemDTO + > { + const toCreate = Array.isArray(input) ? input : [input] + + const created = await this.createReservationItems_(toCreate, context) + + context.messageAggregator?.saveRawMessageData( + created.map((reservationItem) => ({ + eventName: InventoryEvents.reservation_item_created, + metadata: { + service: this.constructor.name, + action: CommonEvents.CREATED, + object: "reservation-item", + }, + data: { id: reservationItem.id }, + })) + ) + + const serializedReservations = await this.baseRepository_.serialize< + InventoryNext.ReservationItemDTO[] | InventoryNext.ReservationItemDTO + >(created, { + populate: true, + }) + + return Array.isArray(input) + ? serializedReservations + : serializedReservations[0] + } + + @InjectTransactionManager("baseRepository_") + async createReservationItems_( + input: InventoryNext.CreateReservationItemInput[], + @MedusaContext() context: Context = {} + ): Promise { + const inventoryLevels = await this.ensureInventoryLevels( + input.map(({ location_id, inventory_item_id }) => ({ + location_id, + inventory_item_id, + })), + context + ) + const created = await this.reservationItemService_.create(input, context) + + const adjustments: Map> = input.reduce( + (acc, curr) => { + const locationMap = acc.get(curr.inventory_item_id) ?? new Map() + + const adjustment = locationMap.get(curr.location_id) ?? 0 + locationMap.set(curr.location_id, adjustment + curr.quantity) + + acc.set(curr.inventory_item_id, locationMap) + return acc + }, + new Map() + ) + + const levelAdjustmentUpdates = inventoryLevels.map((level) => { + const adjustment = adjustments + .get(level.inventory_item_id) + ?.get(level.location_id) + + if (!adjustment) { + return + } + + return { + id: level.id, + reserved_quantity: level.reserved_quantity + adjustment, + } + }) + + await this.inventoryLevelService_.update(levelAdjustmentUpdates, context) + + return created + } + + /** + * Creates an inventory item + * @param input - the input object + * @param context + * @return The created inventory item + */ + create( + input: InventoryNext.CreateInventoryItemInput, + context?: Context + ): Promise + create( + input: InventoryNext.CreateInventoryItemInput[], + context?: Context + ): Promise + + @InjectManager("baseRepository_") + @EmitEvents() + async create( + input: + | InventoryNext.CreateInventoryItemInput + | InventoryNext.CreateInventoryItemInput[], + @MedusaContext() context: Context = {} + ): Promise< + InventoryNext.InventoryItemDTO | InventoryNext.InventoryItemDTO[] + > { + const toCreate = Array.isArray(input) ? input : [input] + + const result = await this.createInventoryItems_(toCreate, context) + + context.messageAggregator?.saveRawMessageData( + result.map((inventoryItem) => ({ + eventName: InventoryEvents.created, + metadata: { + service: this.constructor.name, + action: CommonEvents.CREATED, + object: "inventory-item", + }, + data: { id: inventoryItem.id }, + })) + ) + + const serializedItems = await this.baseRepository_.serialize< + InventoryNext.InventoryItemDTO | InventoryNext.InventoryItemDTO[] + >(result, { + populate: true, + }) + + return Array.isArray(input) ? serializedItems : serializedItems[0] + } + + @InjectTransactionManager("baseRepository_") + async createInventoryItems_( + input: InventoryNext.CreateInventoryItemInput[], + @MedusaContext() context: Context = {} + ): Promise { + return await this.inventoryItemService_.create(input) + } + + createInventoryLevels( + input: InventoryNext.CreateInventoryLevelInput, + context?: Context + ): Promise + createInventoryLevels( + input: InventoryNext.CreateInventoryLevelInput[], + context?: Context + ): Promise + + @InjectManager("baseRepository_") + @EmitEvents() + async createInventoryLevels( + input: + | InventoryNext.CreateInventoryLevelInput[] + | InventoryNext.CreateInventoryLevelInput, + @MedusaContext() context: Context = {} + ): Promise< + InventoryNext.InventoryLevelDTO[] | InventoryNext.InventoryLevelDTO + > { + const toCreate = Array.isArray(input) ? input : [input] + + const created = await this.createInventoryLevels_(toCreate, context) + + context.messageAggregator?.saveRawMessageData( + created.map((inventoryLevel) => ({ + eventName: InventoryEvents.inventory_level_created, + metadata: { + service: this.constructor.name, + action: CommonEvents.CREATED, + object: "inventory-level", + }, + data: { id: inventoryLevel.id }, + })) + ) + + const serialized = await this.baseRepository_.serialize< + InventoryNext.InventoryLevelDTO[] | InventoryNext.InventoryLevelDTO + >(created, { + populate: true, + }) + + return Array.isArray(input) ? serialized : serialized[0] + } + + @InjectTransactionManager("baseRepository_") + async createInventoryLevels_( + input: InventoryNext.CreateInventoryLevelInput[], + @MedusaContext() context: Context = {} + ): Promise { + return await this.inventoryLevelService_.create(input, context) + } + + /** + * Updates inventory items + * @param inventoryItemId - the id of the inventory item to update + * @param input - the input object + * @param context + * @return The updated inventory item + */ + update( + input: InventoryNext.UpdateInventoryItemInput[], + context?: Context + ): Promise + update( + input: InventoryNext.UpdateInventoryItemInput, + context?: Context + ): Promise + + @InjectManager("baseRepository_") + @EmitEvents() + async update( + input: + | InventoryNext.UpdateInventoryItemInput + | InventoryNext.UpdateInventoryItemInput[], + @MedusaContext() context: Context = {} + ): Promise< + InventoryNext.InventoryItemDTO | InventoryNext.InventoryItemDTO[] + > { + const updates = Array.isArray(input) ? input : [input] + + const result = await this.updateInventoryItems_(updates, context) + + context.messageAggregator?.saveRawMessageData( + result.map((inventoryItem) => ({ + eventName: InventoryEvents.updated, + metadata: { + service: this.constructor.name, + action: CommonEvents.UPDATED, + object: "inventory-item", + }, + data: { id: inventoryItem.id }, + })) + ) + + const serializedItems = await this.baseRepository_.serialize< + InventoryNext.InventoryItemDTO | InventoryNext.InventoryItemDTO[] + >(result, { + populate: true, + }) + + return Array.isArray(input) ? serializedItems : serializedItems[0] + } + + @InjectTransactionManager("baseRepository_") + async updateInventoryItems_( + input: (Partial & { id: string })[], + @MedusaContext() context: Context = {} + ): Promise { + return await this.inventoryItemService_.update(input, context) + } + + @InjectTransactionManager("baseRepository_") + @EmitEvents() + async deleteInventoryItemLevelByLocationId( + locationId: string | string[], + @MedusaContext() context: Context = {} + ): Promise<[object[], Record]> { + const result = await this.inventoryLevelService_.softDelete( + { location_id: locationId }, + context + ) + + context.messageAggregator?.saveRawMessageData( + result[0].map((inventoryLevel) => ({ + eventName: InventoryEvents.inventory_level_deleted, + metadata: { + service: this.constructor.name, + action: CommonEvents.DELETED, + object: "inventory-level", + }, + data: { id: inventoryLevel.id }, + })) + ) + + return result + } + + /** + * Deletes an inventory level + * @param inventoryItemId - the id of the inventory item associated with the level + * @param locationId - the id of the location associated with the level + * @param context + */ + @InjectTransactionManager("baseRepository_") + async deleteInventoryLevel( + inventoryItemId: string, + locationId: string, + @MedusaContext() context: Context = {} + ): Promise { + const [inventoryLevel] = await this.inventoryLevelService_.list( + { inventory_item_id: inventoryItemId, location_id: locationId }, + { take: 1 }, + context + ) + + context.messageAggregator?.saveRawMessageData({ + eventName: InventoryEvents.inventory_level_deleted, + metadata: { + service: this.constructor.name, + action: CommonEvents.DELETED, + object: "inventory-level", + }, + data: { id: inventoryLevel.id }, + }) + + if (!inventoryLevel) { + return + } + + return await this.inventoryLevelService_.delete(inventoryLevel.id, context) + } + + async updateInventoryLevels( + updates: InventoryTypes.BulkUpdateInventoryLevelInput[], + context?: Context + ): Promise + async updateInventoryLevels( + updates: InventoryTypes.BulkUpdateInventoryLevelInput, + context?: Context + ): Promise + + @InjectManager("baseRepository_") + @EmitEvents() + async updateInventoryLevels( + updates: + | InventoryTypes.BulkUpdateInventoryLevelInput[] + | InventoryTypes.BulkUpdateInventoryLevelInput, + @MedusaContext() context: Context = {} + ): Promise< + InventoryNext.InventoryLevelDTO | InventoryNext.InventoryLevelDTO[] + > { + const input = Array.isArray(updates) ? updates : [updates] + + const levels = await this.updateInventoryLevels_(input, context) + + context.messageAggregator?.saveRawMessageData( + levels.map((inventoryLevel) => ({ + eventName: InventoryEvents.inventory_level_updated, + metadata: { + service: this.constructor.name, + action: CommonEvents.UPDATED, + object: "inventory-level", + }, + data: { id: inventoryLevel.id }, + })) + ) + + const updatedLevels = await this.baseRepository_.serialize< + | InventoryTypes.InventoryNext.InventoryLevelDTO + | InventoryTypes.InventoryNext.InventoryLevelDTO[] + >(levels, { + populate: true, + }) + + return Array.isArray(updates) ? updatedLevels : updatedLevels[0] + } + + @InjectTransactionManager("baseRepository_") + async updateInventoryLevels_( + updates: InventoryTypes.BulkUpdateInventoryLevelInput[], + @MedusaContext() context: Context = {} + ) { + const inventoryLevels = await this.ensureInventoryLevels( + updates.map(({ location_id, inventory_item_id }) => ({ + location_id, + inventory_item_id, + })), + context + ) + + 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 this.inventoryLevelService_.update( + updates.map((update) => { + const id = levelMap + .get(update.inventory_item_id) + .get(update.location_id) + + return { id, ...update } + }), + context + ) + } + + /** + * Updates a reservation item + * @param reservationItemId + * @param input - the input object + * @param context + * @param context + * @return The updated inventory level + */ + async updateReservationItems( + input: InventoryNext.UpdateReservationItemInput[], + context?: Context + ): Promise + async updateReservationItems( + input: InventoryNext.UpdateReservationItemInput, + context?: Context + ): Promise + + @InjectManager("baseRepository_") + @EmitEvents() + async updateReservationItems( + input: + | InventoryNext.UpdateReservationItemInput + | InventoryNext.UpdateReservationItemInput[], + @MedusaContext() context: Context = {} + ): Promise< + InventoryNext.ReservationItemDTO | InventoryNext.ReservationItemDTO[] + > { + const update = Array.isArray(input) ? input : [input] + + const result = await this.updateReservationItems_(update, context) + + context.messageAggregator?.saveRawMessageData( + result.map((reservationItem) => ({ + eventName: InventoryEvents.inventory_level_updated, + metadata: { + service: this.constructor.name, + action: CommonEvents.UPDATED, + object: "reservation-item", + }, + data: { id: reservationItem.id }, + })) + ) + + const serialized = await this.baseRepository_.serialize< + InventoryNext.ReservationItemDTO | InventoryNext.ReservationItemDTO[] + >(result, { + populate: true, + }) + + return Array.isArray(input) ? serialized : serialized[0] + } + + @InjectTransactionManager("baseRepository_") + async updateReservationItems_( + input: (InventoryNext.UpdateReservationItemInput & { id: string })[], + @MedusaContext() context: Context = {} + ): Promise { + const reservationItems = await this.listReservationItems( + { id: input.map((u) => u.id) }, + {}, + context + ) + + const reservationMap: Map = new Map( + reservationItems.map((r) => [r.id, r]) + ) + + const adjustments: Map> = input.reduce( + (acc, update) => { + const reservation = reservationMap.get(update.id) + if (!reservation) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Reservation item with id ${update.id} not found` + ) + } + + const locationMap = acc.get(reservation.inventory_item_id) ?? new Map() + + if ( + isDefined(update.location_id) && + update.location_id !== reservation.location_id + ) { + const reservationLocationAdjustment = + locationMap.get(reservation.location_id) ?? 0 + + locationMap.set( + reservation.location_id, + reservationLocationAdjustment - reservation.quantity + ) + + const updateLocationAdjustment = + locationMap.get(update.location_id) ?? 0 + + locationMap.set( + update.location_id, + updateLocationAdjustment + (update.quantity || reservation.quantity) + ) + } else if ( + isDefined(update.quantity) && + update.quantity !== reservation.quantity + ) { + const locationAdjustment = + locationMap.get(reservation.location_id) ?? 0 + + locationMap.set( + reservation.location_id, + locationAdjustment + (update.quantity! - reservation.quantity) + ) + } + + acc.set(reservation.inventory_item_id, locationMap) + return acc + }, + new Map() + ) + + const result = await this.reservationItemService_.update(input, context) + + const inventoryLevels = await this.ensureInventoryLevels( + reservationItems.map((r) => ({ + inventory_item_id: r.inventory_item_id, + location_id: r.location_id, + })), + context + ) + + const levelAdjustmentUpdates = inventoryLevels.map((level) => { + const adjustment = adjustments + .get(level.inventory_item_id) + ?.get(level.location_id) + + if (!adjustment) { + return + } + + return { + id: level.id, + reserved_quantity: level.reserved_quantity + adjustment, + } + }) + + await this.inventoryLevelService_.update(levelAdjustmentUpdates, context) + + return result + } + + @InjectTransactionManager("baseRepository_") + @EmitEvents() + async deleteReservationItemByLocationId( + locationId: string | string[], + @MedusaContext() context: Context = {} + ): Promise { + const reservations: InventoryNext.ReservationItemDTO[] = + await this.listReservationItems({ location_id: locationId }, {}, context) + + await this.reservationItemService_.softDelete( + { location_id: locationId }, + context + ) + + context.messageAggregator?.saveRawMessageData( + reservations.map((reservationItem) => ({ + eventName: InventoryEvents.reservation_item_deleted, + metadata: { + service: this.constructor.name, + action: CommonEvents.DELETED, + object: "reservation-item", + }, + data: { id: reservationItem.id }, + })) + ) + + await this.adjustInventoryLevelsForReservationsDeletion( + reservations, + context + ) + } + + /** + * Deletes reservation items by line item + * @param lineItemId - the id of the line item associated with the reservation item + * @param context + */ + + @InjectTransactionManager("baseRepository_") + @EmitEvents() + async deleteReservationItemsByLineItem( + lineItemId: string | string[], + @MedusaContext() context: Context = {} + ): Promise { + const reservations: InventoryNext.ReservationItemDTO[] = + await this.listReservationItems({ line_item_id: lineItemId }, {}, context) + + await this.reservationItemService_.softDelete( + { line_item_id: lineItemId }, + context + ) + + await this.adjustInventoryLevelsForReservationsDeletion( + reservations, + context + ) + + context.messageAggregator?.saveRawMessageData( + reservations.map((reservationItem) => ({ + eventName: InventoryEvents.reservation_item_deleted, + metadata: { + service: this.constructor.name, + action: CommonEvents.DELETED, + object: "reservation-item", + }, + data: { id: reservationItem.id }, + })) + ) + } + + /** + * Adjusts the inventory level for a given inventory item and location. + * @param inventoryItemId - the id of the inventory item + * @param locationId - the id of the location + * @param adjustment - the number to adjust the inventory by (can be positive or negative) + * @param context + * @return The updated inventory level + * @throws when the inventory level is not found + */ + @InjectManager("baseRepository_") + @EmitEvents() + async adjustInventory( + inventoryItemId: string, + locationId: string, + adjustment: number, + @MedusaContext() context: Context = {} + ): Promise { + const result = await this.adjustInventory_( + inventoryItemId, + locationId, + adjustment, + context + ) + + context.messageAggregator?.saveRawMessageData({ + eventName: InventoryEvents.inventory_level_updated, + metadata: { + service: this.constructor.name, + action: CommonEvents.UPDATED, + object: "inventory-level", + }, + data: { id: result.id }, + }) + + return await this.baseRepository_.serialize( + result, + { + populate: true, + } + ) + } + + @InjectTransactionManager("baseRepository_") + async adjustInventory_( + inventoryItemId: string, + locationId: string, + adjustment: number, + @MedusaContext() context: Context = {} + ): Promise { + const inventoryLevel = await this.retrieveInventoryLevelByItemAndLocation( + inventoryItemId, + locationId, + context + ) + + const result = await this.inventoryLevelService_.update( + { + id: inventoryLevel.id, + stocked_quantity: inventoryLevel.stocked_quantity + adjustment, + }, + context + ) + + return result[0] + } + + @InjectManager("baseRepository_") + async retrieveInventoryLevelByItemAndLocation( + inventoryItemId: string, + locationId: string, + @MedusaContext() context: Context = {} + ): Promise { + const [inventoryLevel] = await this.listInventoryLevels( + { inventory_item_id: inventoryItemId, location_id: locationId }, + { take: 1 }, + context + ) + + if (!inventoryLevel) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Inventory level for item ${inventoryItemId} and location ${locationId} not found` + ) + } + + return inventoryLevel + } + + /** + * Retrieves the available quantity of a given inventory item in a given location. + * @param inventoryItemId - the id of the inventory item + * @param locationIds - the ids of the locations to check + * @param context + * @return The available quantity + * @throws when the inventory item is not found + */ + @InjectManager("baseRepository_") + async retrieveAvailableQuantity( + inventoryItemId: string, + locationIds: string[], + @MedusaContext() context: Context = {} + ): Promise { + if (locationIds.length === 0) { + return 0 + } + + await this.inventoryItemService_.retrieve( + inventoryItemId, + { + select: ["id"], + }, + context + ) + + const availableQuantity = + await this.inventoryLevelService_.getAvailableQuantity( + inventoryItemId, + locationIds, + context + ) + + return availableQuantity + } + + /** + * Retrieves the stocked quantity of a given inventory item in a given location. + * @param inventoryItemId - the id of the inventory item + * @param locationIds - the ids of the locations to check + * @param context + * @return The stocked quantity + * @throws when the inventory item is not found + */ + @InjectManager("baseRepository_") + async retrieveStockedQuantity( + inventoryItemId: string, + locationIds: string[], + @MedusaContext() context: Context = {} + ): Promise { + if (locationIds.length === 0) { + return 0 + } + + // Throws if item does not exist + await this.inventoryItemService_.retrieve( + inventoryItemId, + { + select: ["id"], + }, + context + ) + + const stockedQuantity = + await this.inventoryLevelService_.retrieveStockedQuantity( + inventoryItemId, + locationIds, + context + ) + + return stockedQuantity + } + + /** + * Retrieves the reserved quantity of a given inventory item in a given location. + * @param inventoryItemId - the id of the inventory item + * @param locationIds - the ids of the locations to check + * @param context + * @return The reserved quantity + * @throws when the inventory item is not found + */ + @InjectManager("baseRepository_") + async retrieveReservedQuantity( + inventoryItemId: string, + locationIds: string[], + @MedusaContext() context: Context = {} + ): Promise { + // Throws if item does not exist + await this.inventoryItemService_.retrieve( + inventoryItemId, + { + select: ["id"], + }, + context + ) + + if (locationIds.length === 0) { + return 0 + } + + const reservedQuantity = + await this.inventoryLevelService_.getReservedQuantity( + inventoryItemId, + locationIds, + context + ) + + return reservedQuantity + } + + /** + * Confirms whether there is sufficient inventory for a given quantity of a given inventory item in a given location. + * @param inventoryItemId - the id of the inventory item + * @param locationIds - the ids of the locations to check + * @param quantity - the quantity to check + * @param context + * @return Whether there is sufficient inventory + */ + @InjectManager("baseRepository_") + async confirmInventory( + inventoryItemId: string, + locationIds: string[], + quantity: number, + @MedusaContext() context: Context = {} + ): Promise { + const availableQuantity = await this.retrieveAvailableQuantity( + inventoryItemId, + locationIds, + context + ) + return availableQuantity >= quantity + } + + private async adjustInventoryLevelsForReservationsDeletion( + reservations: ReservationItemDTO[], + context: Context + ): Promise { + const inventoryLevels = await this.ensureInventoryLevels( + reservations.map((r) => ({ + inventory_item_id: r.inventory_item_id, + location_id: r.location_id, + })), + context + ) + + const inventoryLevelAdjustments: Map< + string, + Map + > = reservations.reduce((acc, curr) => { + const inventoryLevelMap = acc.get(curr.inventory_item_id) ?? new Map() + + const adjustment = inventoryLevelMap.has(curr.location_id) + ? inventoryLevelMap.get(curr.location_id) - curr.quantity + : -curr.quantity + + inventoryLevelMap.set(curr.location_id, adjustment) + acc.set(curr.inventory_item_id, inventoryLevelMap) + return acc + }, new Map()) + + const levelAdjustmentUpdates = inventoryLevels.map((level) => { + const adjustment = inventoryLevelAdjustments + .get(level.inventory_item_id) + ?.get(level.location_id) + + if (!adjustment) { + return + } + + return { + id: level.id, + reserved_quantity: level.reserved_quantity + adjustment, + } + }) + + await this.inventoryLevelService_.update(levelAdjustmentUpdates, context) + } +} diff --git a/packages/inventory-next/tsconfig.json b/packages/inventory-next/tsconfig.json new file mode 100644 index 0000000000..7adee6410c --- /dev/null +++ b/packages/inventory-next/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "target": "es2020", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": false, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true, // to use ES5 specific tooling + "baseUrl": ".", + "resolveJsonModule": true, + "paths": { + "@models": ["./src/models"], + "@services": ["./src/services"], + "@repositories": ["./src/repositories"], + "@types": ["./src/types"], + "@utils": ["./src/utils"] + } + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/inventory-next/tsconfig.spec.json b/packages/inventory-next/tsconfig.spec.json new file mode 100644 index 0000000000..9b62409191 --- /dev/null +++ b/packages/inventory-next/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/medusa-test-utils/src/module-test-runner.ts b/packages/medusa-test-utils/src/module-test-runner.ts index f80649b14a..ab769a3914 100644 --- a/packages/medusa-test-utils/src/module-test-runner.ts +++ b/packages/medusa-test-utils/src/module-test-runner.ts @@ -22,8 +22,8 @@ export function moduleIntegrationTestRunner({ joinerConfig = [], schema = "public", debug = false, - resolve, testSuite, + resolve, injectedDependencies = {}, }: { moduleName: string @@ -32,8 +32,8 @@ export function moduleIntegrationTestRunner({ joinerConfig?: any[] schema?: string dbName?: string - resolve?: string injectedDependencies?: Record + resolve?: string debug?: boolean testSuite: (options: SuiteOptions) => () => void }) { diff --git a/packages/types/src/inventory/bundle.ts b/packages/types/src/inventory/bundle.ts new file mode 100644 index 0000000000..b2e00cf768 --- /dev/null +++ b/packages/types/src/inventory/bundle.ts @@ -0,0 +1,2 @@ +export * from "./common/index" +export * from "./mutations" diff --git a/packages/types/src/inventory/common.ts b/packages/types/src/inventory/common.ts index 1e23557240..d8a7b7bab9 100644 --- a/packages/types/src/inventory/common.ts +++ b/packages/types/src/inventory/common.ts @@ -204,7 +204,7 @@ export type InventoryLevelDTO = { /** * @interface - * + * * The filters to apply on retrieved reservation items. */ export type FilterableReservationItemProps = { @@ -214,7 +214,7 @@ export type FilterableReservationItemProps = { id?: string | string[] /** * @ignore - * + * * @privateRemark * This property is not used. */ @@ -247,7 +247,7 @@ export type FilterableReservationItemProps = { /** * @interface - * + * * The filters to apply on retrieved inventory items. */ export type FilterableInventoryItemProps = { @@ -281,12 +281,16 @@ export type FilterableInventoryItemProps = { requires_shipping?: boolean } +export interface UpdateInventoryItemInput + extends Partial { + id: string +} /** * @interface - * + * * The details of the inventory item to be created. */ -export type CreateInventoryItemInput = { +export interface CreateInventoryItemInput { /** * The SKU of the inventory item. */ @@ -347,7 +351,7 @@ export type CreateInventoryItemInput = { /** * @interface - * + * * The details of the reservation item to be created. */ export type CreateReservationItemInput = { @@ -387,7 +391,7 @@ export type CreateReservationItemInput = { /** * @interface - * + * * The filters to apply on retrieved inventory levels. */ export type FilterableInventoryLevelProps = { @@ -415,7 +419,7 @@ export type FilterableInventoryLevelProps = { /** * @interface - * + * * The details of the inventory level to be created. */ export type CreateInventoryLevelInput = { @@ -443,7 +447,7 @@ export type CreateInventoryLevelInput = { /** * @interface - * + * * The attributes to update in an inventory level. */ export type UpdateInventoryLevelInput = { @@ -459,7 +463,7 @@ export type UpdateInventoryLevelInput = { /** * @interface - * + * * The attributes to update in an inventory level. The inventory level is identified by the IDs of its associated inventory item and location. */ export type BulkUpdateInventoryLevelInput = { @@ -475,7 +479,7 @@ export type BulkUpdateInventoryLevelInput = { /** * @interface - * + * * The attributes to update in a reservation item. */ export type UpdateReservationItemInput = { diff --git a/packages/types/src/inventory/common/index.ts b/packages/types/src/inventory/common/index.ts new file mode 100644 index 0000000000..1868bcc4f1 --- /dev/null +++ b/packages/types/src/inventory/common/index.ts @@ -0,0 +1,3 @@ +export * from "./inventory-item" +export * from "./inventory-level" +export * from "./reservation-item" diff --git a/packages/types/src/inventory/common/inventory-item.ts b/packages/types/src/inventory/common/inventory-item.ts new file mode 100644 index 0000000000..64890fe6cd --- /dev/null +++ b/packages/types/src/inventory/common/inventory-item.ts @@ -0,0 +1,124 @@ +import { StringComparisonOperator } from "../../common" + +/** + * @schema InventoryItemDTO + * type: object + * required: + * - sku + * properties: + * id: + * description: The inventory item's ID. + * type: string + * example: "iitem_12334" + * sku: + * description: The Stock Keeping Unit (SKU) code of the Inventory Item. + * type: string + * hs_code: + * description: The Harmonized System code of the Inventory Item. May be used by Fulfillment Providers to pass customs information to shipping carriers. + * type: string + * origin_country: + * description: The country in which the Inventory Item was produced. May be used by Fulfillment Providers to pass customs information to shipping carriers. + * type: string + * mid_code: + * description: The Manufacturers Identification code that identifies the manufacturer of the Inventory Item. May be used by Fulfillment Providers to pass customs information to shipping carriers. + * type: string + * title: + * description: "Title of the inventory item" + * type: string + * description: + * description: "Description of the inventory item" + * type: string + * thumbnail: + * description: "Thumbnail for the inventory item" + * type: string + * material: + * description: The material and composition that the Inventory Item is made of, May be used by Fulfillment Providers to pass customs information to shipping carriers. + * type: string + * weight: + * description: The weight of the Inventory Item. May be used in shipping rate calculations. + * type: number + * height: + * description: The height of the Inventory Item. May be used in shipping rate calculations. + * type: number + * width: + * description: The width of the Inventory Item. May be used in shipping rate calculations. + * type: number + * length: + * description: The length of the Inventory Item. May be used in shipping rate calculations. + * type: number + * requires_shipping: + * description: Whether the item requires shipping. + * type: boolean + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + * created_at: + * type: string + * description: "The date with timezone at which the resource was created." + * format: date-time + * updated_at: + * type: string + * description: "The date with timezone at which the resource was updated." + * format: date-time + * deleted_at: + * type: string + * description: "The date with timezone at which the resource was deleted." + * format: date-time + */ +export interface InventoryItemDTO { + id: string + sku?: string | null + origin_country?: string | null + hs_code?: string | null + requires_shipping: boolean + mid_code?: string | null + material?: string | null + weight?: number | null + length?: number | null + height?: number | null + width?: number | null + title?: string | null + description?: string | null + thumbnail?: string | null + metadata?: Record | null + created_at: string | Date + updated_at: string | Date + deleted_at: string | Date | null +} + +/** + * @interface + * + * The filters to apply on retrieved inventory items. + */ +export interface FilterableInventoryItemProps { + /** + * The IDs to filter inventory items by. + */ + id?: string | string[] + /** + * Filter inventory items by the ID of their associated location. + */ + location_id?: string | string[] + /** + * Search term to search inventory items' attributes. + */ + q?: string + /** + * The SKUs to filter inventory items by. + */ + sku?: string | string[] | StringComparisonOperator + /** + * The origin country to filter inventory items by. + */ + origin_country?: string | string[] + /** + * The HS Codes to filter inventory items by. + */ + hs_code?: string | string[] | StringComparisonOperator + /** + * Filter inventory items by whether they require shipping. + */ + requires_shipping?: boolean +} diff --git a/packages/types/src/inventory/common/inventory-level.ts b/packages/types/src/inventory/common/inventory-level.ts new file mode 100644 index 0000000000..bf0814d8e4 --- /dev/null +++ b/packages/types/src/inventory/common/inventory-level.ts @@ -0,0 +1,76 @@ +import { NumericalComparisonOperator } from "../../common" + +/** + * @schema InventoryLevelDTO + * type: object + * required: + * - inventory_item_id + * - location_id + * - stocked_quantity + * - reserved_quantity + * - incoming_quantity + * properties: + * location_id: + * description: the item location ID + * type: string + * stocked_quantity: + * description: the total stock quantity of an inventory item at the given location ID + * type: number + * reserved_quantity: + * description: the reserved stock quantity of an inventory item at the given location ID + * type: number + * incoming_quantity: + * description: the incoming stock quantity of an inventory item at the given location ID + * type: number + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + * created_at: + * type: string + * description: "The date with timezone at which the resource was created." + * format: date-time + * updated_at: + * type: string + * description: "The date with timezone at which the resource was updated." + * format: date-time + * deleted_at: + * type: string + * description: "The date with timezone at which the resource was deleted." + * format: date-time + */ +export interface InventoryLevelDTO { + id: string + inventory_item_id: string + location_id: string + stocked_quantity: number + reserved_quantity: number + incoming_quantity: number + metadata: Record | null + created_at: string | Date + updated_at: string | Date + deleted_at: string | Date | null +} + +export interface FilterableInventoryLevelProps { + /** + * Filter inventory levels by the ID of their associated inventory item. + */ + inventory_item_id?: string | string[] + /** + * Filter inventory levels by the ID of their associated inventory location. + */ + location_id?: string | string[] + /** + * Filters to apply on inventory levels' `stocked_quantity` attribute. + */ + stocked_quantity?: number | NumericalComparisonOperator + /** + * Filters to apply on inventory levels' `reserved_quantity` attribute. + */ + reserved_quantity?: number | NumericalComparisonOperator + /** + * Filters to apply on inventory levels' `incoming_quantity` attribute. + */ + incoming_quantity?: number | NumericalComparisonOperator +} diff --git a/packages/types/src/inventory/common/reservation-item.ts b/packages/types/src/inventory/common/reservation-item.ts new file mode 100644 index 0000000000..2f3a075189 --- /dev/null +++ b/packages/types/src/inventory/common/reservation-item.ts @@ -0,0 +1,107 @@ +import { + NumericalComparisonOperator, + StringComparisonOperator, +} from "../../common" + +/** + * @schema ReservationItemDTO + * title: "Reservation item" + * description: "Represents a reservation of an inventory item at a stock location" + * type: object + * required: + * - id + * - location_id + * - inventory_item_id + * - quantity + * properties: + * id: + * description: "The id of the reservation item" + * type: string + * location_id: + * description: "The id of the location of the reservation" + * type: string + * inventory_item_id: + * description: "The id of the inventory item the reservation relates to" + * type: string + * description: + * description: "Description of the reservation item" + * type: string + * created_by: + * description: "UserId of user who created the reservation item" + * type: string + * quantity: + * description: "The id of the reservation item" + * type: number + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + * created_at: + * type: string + * description: "The date with timezone at which the resource was created." + * format: date-time + * updated_at: + * type: string + * description: "The date with timezone at which the resource was updated." + * format: date-time + * deleted_at: + * type: string + * description: "The date with timezone at which the resource was deleted." + * format: date-time + */ +export interface ReservationItemDTO { + id: string + location_id: string + inventory_item_id: string + quantity: number + line_item_id?: string | null + description?: string | null + created_by?: string | null + metadata: Record | null + created_at: string | Date + updated_at: string | Date + deleted_at: string | Date | null +} + +/** + * @interface + * + * The filters to apply on retrieved reservation items. + */ +export interface FilterableReservationItemProps { + /** + * The IDs to filter reservation items by. + */ + id?: string | string[] + /** + * @ignore + * + * @privateRemark + * This property is not used. + */ + type?: string | string[] + /** + * Filter reservation items by the ID of their associated line item. + */ + line_item_id?: string | string[] + /** + * Filter reservation items by the ID of their associated inventory item. + */ + inventory_item_id?: string | string[] + /** + * Filter reservation items by the ID of their associated location. + */ + location_id?: string | string[] + /** + * Description filters to apply on the reservation items' `description` attribute. + */ + description?: string | StringComparisonOperator + /** + * The "created by" values to filter reservation items by. + */ + created_by?: string | string[] + /** + * Filters to apply on the reservation items' `quantity` attribute. + */ + quantity?: number | NumericalComparisonOperator +} diff --git a/packages/types/src/inventory/index.ts b/packages/types/src/inventory/index.ts index eade309433..3b728a9630 100644 --- a/packages/types/src/inventory/index.ts +++ b/packages/types/src/inventory/index.ts @@ -1,2 +1,4 @@ +export * as InventoryNext from "./bundle" export * from "./common" export * from "./service" +export * from "./service-next" diff --git a/packages/types/src/inventory/mutations/index.ts b/packages/types/src/inventory/mutations/index.ts new file mode 100644 index 0000000000..1868bcc4f1 --- /dev/null +++ b/packages/types/src/inventory/mutations/index.ts @@ -0,0 +1,3 @@ +export * from "./inventory-item" +export * from "./inventory-level" +export * from "./reservation-item" diff --git a/packages/types/src/inventory/mutations/inventory-item.ts b/packages/types/src/inventory/mutations/inventory-item.ts new file mode 100644 index 0000000000..0bbc974a84 --- /dev/null +++ b/packages/types/src/inventory/mutations/inventory-item.ts @@ -0,0 +1,67 @@ +export interface UpdateInventoryItemInput + extends Partial { + id: string +} +/** + * @interface + * + * The details of the inventory item to be created. + */ +export interface CreateInventoryItemInput { + /** + * The SKU of the inventory item. + */ + sku?: string | null + /** + * The origin country of the inventory item. + */ + origin_country?: string | null + /** + * The MID code of the inventory item. + */ + mid_code?: string | null + /** + * The material of the inventory item. + */ + material?: string | null + /** + * The weight of the inventory item. + */ + weight?: number | null + /** + * The length of the inventory item. + */ + length?: number | null + /** + * The height of the inventory item. + */ + height?: number | null + /** + * The width of the inventory item. + */ + width?: number | null + /** + * The title of the inventory item. + */ + title?: string | null + /** + * The description of the inventory item. + */ + description?: string | null + /** + * The thumbnail of the inventory item. + */ + thumbnail?: string | null + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record | null + /** + * The HS code of the inventory item. + */ + hs_code?: string | null + /** + * Whether the inventory item requires shipping. + */ + requires_shipping?: boolean +} diff --git a/packages/types/src/inventory/mutations/inventory-level.ts b/packages/types/src/inventory/mutations/inventory-level.ts new file mode 100644 index 0000000000..c4e10df46a --- /dev/null +++ b/packages/types/src/inventory/mutations/inventory-level.ts @@ -0,0 +1,58 @@ +export interface CreateInventoryLevelInput { + /** + * The ID of the associated inventory item. + */ + inventory_item_id: string + /** + * The ID of the associated location. + */ + location_id: string + /** + * The stocked quantity of the associated inventory item in the associated location. + */ + stocked_quantity: number + /** + * The reserved quantity of the associated inventory item in the associated location. + */ + reserved_quantity?: number + /** + * The incoming quantity of the associated inventory item in the associated location. + */ + incoming_quantity?: number +} + +/** + * @interface + * + * The attributes to update in an inventory level. + */ +export interface UpdateInventoryLevelInput { + /** + * id of the inventory level to update + */ + id: string + /** + * The stocked quantity of the associated inventory item in the associated location. + */ + stocked_quantity?: number + /** + * The incoming quantity of the associated inventory item in the associated location. + */ + incoming_quantity?: number +} + +/** + * @interface + * + * The attributes to update in an inventory level. The inventory level is identified by the IDs of its associated inventory item and location. + */ +export type BulkUpdateInventoryLevelInput = { + /** + * The ID of the associated inventory level. + */ + inventory_item_id: string + /** + * The ID of the associated location. + */ + location_id: string +} & UpdateInventoryLevelInput diff --git a/packages/types/src/inventory/mutations/reservation-item.ts b/packages/types/src/inventory/mutations/reservation-item.ts new file mode 100644 index 0000000000..e5a712fadf --- /dev/null +++ b/packages/types/src/inventory/mutations/reservation-item.ts @@ -0,0 +1,70 @@ +/** + * @interface + * + * The attributes to update in a reservation item. + */ +export interface UpdateReservationItemInput { + id: string + /** + * The reserved quantity. + */ + quantity?: number + /** + * The ID of the associated location. + */ + location_id?: string + /** + * The description of the reservation item. + */ + description?: string + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record | null +} + +/** + * @interface + * + * The details of the reservation item to be created. + */ +export interface CreateReservationItemInput { + /** + * The ID of the associated line item. + */ + line_item_id?: string + /** + * The ID of the associated inventory item. + */ + inventory_item_id: string + /** + * The ID of the associated location. + */ + location_id: string + /** + * The reserved quantity. + */ + quantity: number + /** + * The description of the reservation. + */ + description?: string + /** + * The user or system that created the reservation. Can be any form of identification string. + */ + created_by?: string + /** + * An ID associated with an external third-party system that the reservation item is connected to. + */ + external_id?: string + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record | null +} + +export interface ReserveQuantityContext { + locationId?: string + lineItemId?: string + salesChannelId?: string | null +} diff --git a/packages/types/src/inventory/service-next.ts b/packages/types/src/inventory/service-next.ts new file mode 100644 index 0000000000..a3e9659be8 --- /dev/null +++ b/packages/types/src/inventory/service-next.ts @@ -0,0 +1,963 @@ +import { RestoreReturn, SoftDeleteReturn } from "../dal" + +import { Context } from "../shared-context" +import { FindConfig } from "../common" +import { IModuleService } from "../modules-sdk" +import { InventoryNext } from "." + +/** + * The main service interface for the inventory module. + */ +export interface IInventoryServiceNext extends IModuleService { + /** + * This method is used to retrieve a paginated list of inventory items along with the total count of available inventory items satisfying the provided filters. + * @param {FilterableInventoryItemProps} selector - The filters to apply on the retrieved inventory items. + * @param {FindConfig} config - + * The configurations determining how the inventory items are retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a inventory item. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @return {Promise<[InventoryItemDTO[], number]>} The list of inventory items along with the total count. + * + * @example + * To retrieve a list of inventory items using their IDs: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveInventoryItems (ids: string[]) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const [inventoryItems, count] = await inventoryModule.listInventoryItems({ + * id: ids + * }) + * + * // do something with the inventory items or return them + * } + * ``` + * + * To specify relations that should be retrieved within the inventory items: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveInventoryItems (ids: string[]) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const [inventoryItems, count] = await inventoryModule.listInventoryItems({ + * id: ids + * }, { + * relations: ["inventory_level"] + * }) + * + * // do something with the inventory items or return them + * } + * ``` + * + * By default, only the first `10` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveInventoryItems (ids: string[], skip: number, take: number) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const [inventoryItems, count] = await inventoryModule.listInventoryItems({ + * id: ids + * }, { + * relations: ["inventory_level"], + * skip, + * take + * }) + * + * // do something with the inventory items or return them + * } + * ``` + */ + list( + selector: InventoryNext.FilterableInventoryItemProps, + config?: FindConfig, + context?: Context + ): Promise + + listAndCount( + selector: InventoryNext.FilterableInventoryItemProps, + config?: FindConfig, + context?: Context + ): Promise<[InventoryNext.InventoryItemDTO[], number]> + + /** + * This method is used to retrieve a paginated list of reservation items along with the total count of available reservation items satisfying the provided filters. + * @param {FilterableReservationItemProps} selector - The filters to apply on the retrieved reservation items. + * @param {FindConfig} config - + * The configurations determining how the reservation items are retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a reservation item. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @return {Promise<[ReservationItemDTO[], number]>} The list of reservation items along with the total count. + * + * @example + * To retrieve a list of reservation items using their IDs: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveReservationItems (ids: string[]) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const [reservationItems, count] = await inventoryModule.listReservationItems({ + * id: ids + * }) + * + * // do something with the reservation items or return them + * } + * ``` + * + * To specify relations that should be retrieved within the reservation items: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveReservationItems (ids: string[]) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const [reservationItems, count] = await inventoryModule.listReservationItems({ + * id: ids + * }, { + * relations: ["inventory_item"] + * }) + * + * // do something with the reservation items or return them + * } + * ``` + * + * By default, only the first `10` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveReservationItems (ids: string[], skip: number, take: number) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const [reservationItems, count] = await inventoryModule.listReservationItems({ + * id: ids + * }, { + * relations: ["inventory_item"], + * skip, + * take + * }) + * + * // do something with the reservation items or return them + * } + * ``` + */ + listReservationItems( + selector: InventoryNext.FilterableReservationItemProps, + config?: FindConfig, + context?: Context + ): Promise + + listAndCountReservationItems( + selector: InventoryNext.FilterableReservationItemProps, + config?: FindConfig, + context?: Context + ): Promise<[InventoryNext.ReservationItemDTO[], number]> + + /** + * This method is used to retrieve a paginated list of inventory levels along with the total count of available inventory levels satisfying the provided filters. + * @param {FilterableInventoryLevelProps} selector - The filters to apply on the retrieved inventory levels. + * @param {FindConfig} config - + * The configurations determining how the inventory levels are retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a inventory level. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @return {Promise<[InventoryLevelDTO[], number]>} The list of inventory levels along with the total count. + * + * @example + * To retrieve a list of inventory levels using their IDs: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveInventoryLevels (inventoryItemIds: string[]) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const [inventoryLevels, count] = await inventoryModule.listInventoryLevels({ + * inventory_item_id: inventoryItemIds + * }) + * + * // do something with the inventory levels or return them + * } + * ``` + * + * To specify relations that should be retrieved within the inventory levels: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveInventoryLevels (inventoryItemIds: string[]) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const [inventoryLevels, count] = await inventoryModule.listInventoryLevels({ + * inventory_item_id: inventoryItemIds + * }, { + * relations: ["inventory_item"] + * }) + * + * // do something with the inventory levels or return them + * } + * ``` + * + * By default, only the first `10` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveInventoryLevels (inventoryItemIds: string[], skip: number, take: number) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const [inventoryLevels, count] = await inventoryModule.listInventoryLevels({ + * inventory_item_id: inventoryItemIds + * }, { + * relations: ["inventory_item"], + * skip, + * take + * }) + * + * // do something with the inventory levels or return them + * } + * ``` + */ + listInventoryLevels( + selector: InventoryNext.FilterableInventoryLevelProps, + config?: FindConfig, + context?: Context + ): Promise + + listAndCountInventoryLevels( + selector: InventoryNext.FilterableInventoryLevelProps, + config?: FindConfig, + context?: Context + ): Promise<[InventoryNext.InventoryLevelDTO[], number]> + + /** + * This method is used to retrieve an inventory item by its ID + * + * @param {string} inventoryItemId - The ID of the inventory item to retrieve. + * @param {FindConfig} config - + * The configurations determining how the inventory item is retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a inventory item. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The retrieved inventory item. + * + * @example + * A simple example that retrieves a inventory item by its ID: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveInventoryItem (id: string) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const inventoryItem = await inventoryModule.retrieveInventoryItem(id) + * + * // do something with the inventory item or return it + * } + * ``` + * + * To specify relations that should be retrieved: + * + * ```ts + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveInventoryItem (id: string) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const inventoryItem = await inventoryModule.retrieveInventoryItem(id, { + * relations: ["inventory_level"] + * }) + * + * // do something with the inventory item or return it + * } + * ``` + */ + retrieve( + inventoryItemId: string, + config?: FindConfig, + context?: Context + ): Promise + + /** + * This method is used to retrieve an inventory level for an inventory item and a location. + * + * @param {string} inventoryItemId - The ID of the inventory item. + * @param {string} locationId - The ID of the location. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The retrieved inventory level. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveInventoryLevel ( + * inventoryItemId: string, + * locationId: string + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const inventoryLevel = await inventoryModule.retrieveInventoryLevel( + * inventoryItemId, + * locationId + * ) + * + * // do something with the inventory level or return it + * } + */ + retrieveInventoryLevelByItemAndLocation( + inventoryItemId: string, + locationId: string, + context?: Context + ): Promise + + retrieveInventoryLevel( + inventoryLevelId: string, + config?: FindConfig, + context?: Context + ): Promise + + /** + * This method is used to retrieve a reservation item by its ID. + * + * @param {string} reservationId - The ID of the reservation item. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The retrieved reservation item. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveReservationItem (id: string) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const reservationItem = await inventoryModule.retrieveReservationItem(id) + * + * // do something with the reservation item or return it + * } + */ + retrieveReservationItem( + reservationId: string, + config?: FindConfig, + context?: Context + ): Promise + + /** + * This method is used to create reservation items. + * + * @param {CreateReservationItemInput[]} input - The details of the reservation items to create. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns { Promise} The created reservation items' details. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function createReservationItems (items: { + * inventory_item_id: string, + * location_id: string, + * quantity: number + * }[]) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const reservationItems = await inventoryModule.createReservationItems( + * items + * ) + * + * // do something with the reservation items or return them + * } + */ + createReservationItems( + input: InventoryNext.CreateReservationItemInput[], + context?: Context + ): Promise + createReservationItems( + input: InventoryNext.CreateReservationItemInput, + context?: Context + ): Promise + + /** + * This method is used to create inventory items. + * + * @param {CreateInventoryItemInput[]} input - The details of the inventory items to create. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The created inventory items' details. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function createInventoryItems (items: { + * sku: string, + * requires_shipping: boolean + * }[]) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const inventoryItems = await inventoryModule.createInventoryItems( + * items + * ) + * + * // do something with the inventory items or return them + * } + */ + create( + input: InventoryNext.CreateInventoryItemInput[], + context?: Context + ): Promise + create( + input: InventoryNext.CreateInventoryItemInput, + context?: Context + ): Promise + + /** + * This method is used to create inventory levels. + * + * @param {CreateInventoryLevelInput[]} data - The details of the inventory levels to create. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The created inventory levels' details. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function createInventoryLevels (items: { + * inventory_item_id: string + * location_id: string + * stocked_quantity: number + * }[]) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const inventoryLevels = await inventoryModule.createInventoryLevels( + * items + * ) + * + * // do something with the inventory levels or return them + * } + */ + createInventoryLevels( + data: InventoryNext.CreateInventoryLevelInput[], + context?: Context + ): Promise + createInventoryLevels( + data: InventoryNext.CreateInventoryLevelInput, + context?: Context + ): Promise + + /** + * This method is used to update inventory levels. Each inventory level is identified by the IDs of its associated inventory item and location. + * + * @param {BulkUpdateInventoryLevelInput} updates - The attributes to update in each inventory level. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated inventory levels' details. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function updateInventoryLevels (items: { + * inventory_item_id: string, + * location_id: string, + * stocked_quantity: number + * }[]) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const inventoryLevels = await inventoryModule.updateInventoryLevels( + * items + * ) + * + * // do something with the inventory levels or return them + * } + */ + updateInventoryLevels( + updates: InventoryNext.BulkUpdateInventoryLevelInput[], + context?: Context + ): Promise + updateInventoryLevels( + updates: InventoryNext.BulkUpdateInventoryLevelInput, + context?: Context + ): Promise + + /** + * This method is used to update an inventory item. + * + * @param {string} inventoryItemId - The ID of the inventory item. + * @param {Partial} input - The attributes to update in the inventory item. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated inventory item's details. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function updateInventoryItem ( + * inventoryItemId: string, + * sku: string + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const inventoryItem = await inventoryModule.updateInventoryItem( + * inventoryItemId, + * { + * sku + * } + * ) + * + * // do something with the inventory item or return it + * } + */ + update( + input: InventoryNext.UpdateInventoryItemInput, + context?: Context + ): Promise + update( + input: InventoryNext.UpdateInventoryItemInput[], + context?: Context + ): Promise + + /** + * This method is used to update a reservation item. + * + * @param {string} reservationItemId - The ID of the reservation item. + * @param {UpdateReservationItemInput} input - The attributes to update in the reservation item. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated reservation item. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function updateReservationItem ( + * reservationItemId: string, + * quantity: number + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const reservationItem = await inventoryModule.updateReservationItem( + * reservationItemId, + * { + * quantity + * } + * ) + * + * // do something with the reservation item or return it + * } + */ + updateReservationItems( + input: InventoryNext.UpdateReservationItemInput, + context?: Context + ): Promise + updateReservationItems( + input: InventoryNext.UpdateReservationItemInput[], + context?: Context + ): Promise + + /** + * This method is used to delete the reservation items associated with a line item or multiple line items. + * + * @param {string | string[]} lineItemId - The ID(s) of the line item(s). + * @param {Context} context - A context used to share re9sources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the reservation items are successfully deleted. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function deleteReservationItemsByLineItem ( + * lineItemIds: string[] + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * await inventoryModule.deleteReservationItemsByLineItem( + * lineItemIds + * ) + * } + */ + deleteReservationItemsByLineItem( + lineItemId: string | string[], + context?: Context + ): Promise + + /** + * This method is used to delete a reservation item or multiple reservation items by their IDs. + * + * @param {string | string[]} reservationItemId - The ID(s) of the reservation item(s) to delete. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the reservation item(s) are successfully deleted. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function deleteReservationItems ( + * reservationItemIds: string[] + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * await inventoryModule.deleteReservationItem( + * reservationItemIds + * ) + * } + */ + deleteReservationItems( + reservationItemId: string | string[], + context?: Context + ): Promise + + /** + * This method is used to delete an inventory item or multiple inventory items. The inventory items are only soft deleted and can be restored using the + * {@link restoreInventoryItem} method. + * + * @param {string | string[]} inventoryItemId - The ID(s) of the inventory item(s) to delete. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the inventory item(s) are successfully deleted. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function deleteInventoryItem ( + * inventoryItems: string[] + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * await inventoryModule.deleteInventoryItem( + * inventoryItems + * ) + * } + */ + delete(inventoryItemId: string | string[], context?: Context): Promise + + /** + * Soft delete inventory items + * @param inventoryItemIds + * @param config + * @param sharedContext + */ + softDelete( + inventoryItemIds: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + /** + * This method is used to restore an inventory item or multiple inventory items that were previously deleted using the {@link deleteInventoryItem} method. + * + * @param {string[]} inventoryItemId - The ID(s) of the inventory item(s) to restore. + * @param {RestoreReturn} config - Restore config + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the inventory item(s) are successfully restored. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function restoreInventoryItem ( + * inventoryItems: string[] + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * await inventoryModule.restoreInventoryItem( + * inventoryItems + * ) + * } + */ + restore( + inventoryItemIds: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + + /** + * This method deletes the inventory item level(s) for the ID(s) of associated location(s). + * + * @param {string | string[]} locationId - The ID(s) of the associated location(s). + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the inventory item level(s) are successfully restored. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function deleteInventoryItemLevelByLocationId ( + * locationIds: string[] + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * await inventoryModule.deleteInventoryItemLevelByLocationId( + * locationIds + * ) + * } + */ + deleteInventoryItemLevelByLocationId( + locationId: string | string[], + context?: Context + ): Promise<[object[], Record]> + + /** + * This method deletes reservation item(s) by the ID(s) of associated location(s). + * + * @param {string | string[]} locationId - The ID(s) of the associated location(s). + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the reservation item(s) are successfully restored. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function deleteReservationItemByLocationId ( + * locationIds: string[] + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * await inventoryModule.deleteReservationItemByLocationId( + * locationIds + * ) + * } + */ + deleteReservationItemByLocationId( + locationId: string | string[], + context?: Context + ): Promise + + /** + * This method is used to delete an inventory level. The inventory level is identified by the IDs of its associated inventory item and location. + * + * @param {string} inventoryItemId - The ID of the associated inventory item. + * @param {string} locationId - The ID of the associated location. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the inventory level(s) are successfully restored. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function deleteInventoryLevel ( + * inventoryItemId: string, + * locationId: string + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * await inventoryModule.deleteInventoryLevel( + * inventoryItemId, + * locationId + * ) + * } + */ + deleteInventoryLevel( + inventoryItemId: string, + locationId: string, + context?: Context + ): Promise + + /** + * This method is used to adjust the inventory level's stocked quantity. The inventory level is identified by the IDs of its associated inventory item and location. + * + * @param {string} inventoryItemId - The ID of the associated inventory item. + * @param {string} locationId - The ID of the associated location. + * @param {number} adjustment - A positive or negative number used to adjust the inventory level's stocked quantity. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The inventory level's details. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function adjustInventory ( + * inventoryItemId: string, + * locationId: string, + * adjustment: number + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const inventoryLevel = await inventoryModule.adjustInventory( + * inventoryItemId, + * locationId, + * adjustment + * ) + * + * // do something with the inventory level or return it. + * } + */ + adjustInventory( + inventoryItemId: string, + locationId: string, + adjustment: number, + context?: Context + ): Promise + + /** + * This method is used to confirm whether the specified quantity of an inventory item is available in the specified locations. + * + * @param {string} inventoryItemId - The ID of the inventory item to check its availability. + * @param {string[]} locationIds - The IDs of the locations to check the quantity availability in. + * @param {number} quantity - The quantity to check if available for the inventory item in the specified locations. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Whether the specified quantity is available for the inventory item in the specified locations. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function confirmInventory ( + * inventoryItemId: string, + * locationIds: string[], + * quantity: number + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * return await inventoryModule.confirmInventory( + * inventoryItemId, + * locationIds, + * quantity + * ) + * } + */ + confirmInventory( + inventoryItemId: string, + locationIds: string[], + quantity: number, + context?: Context + ): Promise + + /** + * This method is used to retrieve the available quantity of an inventory item within the specified locations. + * + * @param {string} inventoryItemId - The ID of the inventory item to retrieve its quantity. + * @param {string[]} locationIds - The IDs of the locations to retrieve the available quantity from. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The available quantity of the inventory item in the specified locations. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveAvailableQuantity ( + * inventoryItemId: string, + * locationIds: string[], + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const quantity = await inventoryModule.retrieveAvailableQuantity( + * inventoryItemId, + * locationIds, + * ) + * + * // do something with the quantity or return it + * } + */ + retrieveAvailableQuantity( + inventoryItemId: string, + locationIds: string[], + context?: Context + ): Promise + + /** + * This method is used to retrieve the stocked quantity of an inventory item within the specified locations. + * + * @param {string} inventoryItemId - The ID of the inventory item to retrieve its stocked quantity. + * @param {string[]} locationIds - The IDs of the locations to retrieve the stocked quantity from. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The stocked quantity of the inventory item in the specified locations. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveStockedQuantity ( + * inventoryItemId: string, + * locationIds: string[], + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const quantity = await inventoryModule.retrieveStockedQuantity( + * inventoryItemId, + * locationIds, + * ) + * + * // do something with the quantity or return it + * } + */ + retrieveStockedQuantity( + inventoryItemId: string, + locationIds: string[], + context?: Context + ): Promise + + /** + * This method is used to retrieve the reserved quantity of an inventory item within the specified locations. + * + * @param {string} inventoryItemId - The ID of the inventory item to retrieve its reserved quantity. + * @param {string[]} locationIds - The IDs of the locations to retrieve the reserved quantity from. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The reserved quantity of the inventory item in the specified locations. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function retrieveReservedQuantity ( + * inventoryItemId: string, + * locationIds: string[], + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * const quantity = await inventoryModule.retrieveReservedQuantity( + * inventoryItemId, + * locationIds, + * ) + * + * // do something with the quantity or return it + * } + */ + retrieveReservedQuantity( + inventoryItemId: string, + locationIds: string[], + context?: Context + ): Promise +} diff --git a/packages/utils/src/bundles.ts b/packages/utils/src/bundles.ts index cf4d0d8867..582d185d41 100644 --- a/packages/utils/src/bundles.ts +++ b/packages/utils/src/bundles.ts @@ -12,4 +12,5 @@ export * as PromotionUtils from "./promotion" export * as SearchUtils from "./search" export * as ShippingProfileUtils from "./shipping" export * as UserUtils from "./user" +export * as InventoryUtils from "./inventory" export * as ApiKeyUtils from "./api-key" diff --git a/packages/utils/src/common/__tests__/partition-array.spec.ts b/packages/utils/src/common/__tests__/partition-array.spec.ts new file mode 100644 index 0000000000..4fdcbf1281 --- /dev/null +++ b/packages/utils/src/common/__tests__/partition-array.spec.ts @@ -0,0 +1,12 @@ +import { partitionArray } from "../../../dist" + +describe("partitionArray", function () { + it("should split array according to predicate", function () { + const res = partitionArray([1, 2, 3, 4, 5], (x) => x % 2 === 0) + + expect(res).toEqual([ + [2, 4], + [1, 3, 5], + ]) + }) +}) diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index 140bd8a36f..2b81f83fe4 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -32,6 +32,7 @@ export * from "./medusa-container" export * from "./object-from-string-path" export * from "./object-to-string-path" export * from "./optional-numeric-serializer" +export * from "./partition-array" export * from "./pick-deep" export * from "./pick-value-from-object" export * from "./plurailze" diff --git a/packages/utils/src/common/partition-array.ts b/packages/utils/src/common/partition-array.ts new file mode 100644 index 0000000000..7537ac03b2 --- /dev/null +++ b/packages/utils/src/common/partition-array.ts @@ -0,0 +1,30 @@ +/** + * Partitions an array into two arrays based on a predicate function + + * @example + * const result = partitionArray([1, 2, 3, 4, 5], (x) => x % 2 === 0) + * + * console.log(result) + * + * // output: [[2, 4], [1, 3, 5]] + * + * @param {T} input input array of type T + * @param {(T) => boolean} predicate function to use when split array elements + */ +export const partitionArray = ( + input: T[], + predicate: (T) => boolean +): [T[], T[]] => { + return input.reduce( + ([pos, neg], currentElement) => { + if (predicate(currentElement)) { + pos.push(currentElement) + } else { + neg.push(currentElement) + } + + return [pos, neg] + }, + [[], []] as [T[], T[]] + ) +} diff --git a/packages/utils/src/event-bus/common-events.ts b/packages/utils/src/event-bus/common-events.ts index 3fee0d8846..1b273bd957 100644 --- a/packages/utils/src/event-bus/common-events.ts +++ b/packages/utils/src/event-bus/common-events.ts @@ -2,6 +2,7 @@ export enum CommonEvents { CREATED = "created", UPDATED = "updated", DELETED = "deleted", + RESTORED = "restored", ATTACHED = "attached", DETACHED = "detached", } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index a8a2178ebb..0939438ef8 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -8,6 +8,7 @@ export * from "./event-bus" export * from "./exceptions" export * from "./feature-flags" export * from "./fulfillment" +export * from "./inventory" export * from "./modules-sdk" export * from "./orchestration" export * from "./order" diff --git a/packages/utils/src/inventory/events.ts b/packages/utils/src/inventory/events.ts new file mode 100644 index 0000000000..b693c46c32 --- /dev/null +++ b/packages/utils/src/inventory/events.ts @@ -0,0 +1,14 @@ +import { CommonEvents } from "../event-bus" + +export const InventoryEvents = { + created: "inventory-item." + CommonEvents.CREATED, + updated: "inventory-item." + CommonEvents.UPDATED, + deleted: "inventory-item." + CommonEvents.DELETED, + restored: "inventory-item." + CommonEvents.RESTORED, + reservation_item_created: "reservation-item." + CommonEvents.CREATED, + reservation_item_updated: "reservation-item." + CommonEvents.UPDATED, + reservation_item_deleted: "reservation-item." + CommonEvents.DELETED, + inventory_level_deleted: "inventory-level." + CommonEvents.DELETED, + inventory_level_created: "inventory-level." + CommonEvents.CREATED, + inventory_level_updated: "inventory-level." + CommonEvents.UPDATED, +} diff --git a/packages/utils/src/inventory/index.ts b/packages/utils/src/inventory/index.ts new file mode 100644 index 0000000000..92c2484024 --- /dev/null +++ b/packages/utils/src/inventory/index.ts @@ -0,0 +1 @@ +export * from "./events" diff --git a/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts b/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts index 3ae18886fe..a7199d04f4 100644 --- a/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts +++ b/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts @@ -6,10 +6,11 @@ import { ModuleServiceInitializeOptions, RepositoryService, } from "@medusajs/types" + import { asClass } from "awilix" +import { internalModuleServiceFactory } from "../internal-module-service-factory" import { lowerCaseFirst } from "../../common" import { mikroOrmBaseRepositoryFactory } from "../../dal" -import { internalModuleServiceFactory } from "../internal-module-service-factory" type RepositoryLoaderOptions = { moduleModels: Record diff --git a/yarn.lock b/yarn.lock index fe573b1870..0a345a92d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8311,6 +8311,31 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/inventory-next@workspace:packages/inventory-next": + version: 0.0.0-use.local + resolution: "@medusajs/inventory-next@workspace:packages/inventory-next" + dependencies: + "@medusajs/modules-sdk": ^1.12.4 + "@medusajs/types": ^1.11.12 + "@medusajs/utils": ^1.11.1 + "@mikro-orm/cli": 5.9.7 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/migrations": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 + awilix: ^8.0.0 + cross-env: ^5.2.1 + dotenv: 16.4.5 + jest: ^29.6.3 + knex: 2.4.2 + medusa-test-utils: ^1.1.40 + rimraf: ^5.0.1 + ts-jest: ^29.1.1 + ts-node: ^10.9.1 + tsc-alias: ^1.8.6 + typescript: ^5.1.6 + languageName: unknown + linkType: soft + "@medusajs/inventory@workspace:^, @medusajs/inventory@workspace:packages/inventory": version: 0.0.0-use.local resolution: "@medusajs/inventory@workspace:packages/inventory"