chore(): Reorganize modules (#7210)

**What**
Move all modules to the modules directory
This commit is contained in:
Adrien de Peretti
2024-05-02 17:33:34 +02:00
committed by GitHub
parent 7a351eef09
commit 4eae25e1ef
870 changed files with 91 additions and 62 deletions

View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View File

@@ -0,0 +1,25 @@
# @medusajs/inventory-next
## 0.0.3
### Patch Changes
- [#6816](https://github.com/medusajs/medusa/pull/6816) [`6af8f9be6d`](https://github.com/medusajs/medusa/commit/6af8f9be6d04677d95d3eaad5d9045b5e727b1c5) Thanks [@pKorsholm](https://github.com/pKorsholm)! - fix(inventory-next): update reservation item id prefix
- Updated dependencies [[`06f22bb48a`](https://github.com/medusajs/medusa/commit/06f22bb48ad1fe73577657b8c5db055312f16a0d), [`deab12e27e`](https://github.com/medusajs/medusa/commit/deab12e27e8249e26d24d7bc904c18195679ff24), [`56481e683d`](https://github.com/medusajs/medusa/commit/56481e683d33ff98f0d4c4e144873bb23f993c9c), [`9073d7aba3`](https://github.com/medusajs/medusa/commit/9073d7aba3419e4dc0a206473291a46ebd79b8c1), [`4974f5e455`](https://github.com/medusajs/medusa/commit/4974f5e4557bd64a328a881ec02b91e15485bd23), [`05e857d256`](https://github.com/medusajs/medusa/commit/05e857d25657b5576a891c9b48c19c1759c70701), [`1ef9c78cea`](https://github.com/medusajs/medusa/commit/1ef9c78cea080c3b7c136f909c6cddec9d8f0c62)]:
- @medusajs/modules-sdk@1.12.10
- @medusajs/types@1.11.15
- @medusajs/utils@1.11.8
## 0.0.2
### Patch Changes
- [#6708](https://github.com/medusajs/medusa/pull/6708) [`62d5803b20`](https://github.com/medusajs/medusa/commit/62d5803b2085daa682ea9bfbe7a3593057c7da4e) Thanks [@pKorsholm](https://github.com/pKorsholm)! - feat(medusa, core-flows, inventory-next): add delete-inventory-item endpoint
- [#6700](https://github.com/medusajs/medusa/pull/6700) [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Version all modules to allow for initial testing
- Updated dependencies [[`1fd0457c15`](https://github.com/medusajs/medusa/commit/1fd0457c153b2ef7657c052878d8e5364e1b324a), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`d4b921f3db`](https://github.com/medusajs/medusa/commit/d4b921f3dbe0a38f1565a8de759996c70798d58e), [`ac86362e81`](https://github.com/medusajs/medusa/commit/ac86362e81d8523cb8e3dfad026fc94658513018), [`e4acde1aa2`](https://github.com/medusajs/medusa/commit/e4acde1aa2eb57f07e6692fe8b61f728948b9a96), [`1a661adf3e`](https://github.com/medusajs/medusa/commit/1a661adf3ef4991aa6e237dd894b6a5c47cd4aca), [`56cbf88115`](https://github.com/medusajs/medusa/commit/56cbf88115994adea7037c3f2814f0c96af3cfc0), [`36a61658f9`](https://github.com/medusajs/medusa/commit/36a61658f969a7b19c84a1e621ad1464927cafb1), [`04a532e5ef`](https://github.com/medusajs/medusa/commit/04a532e5efabbf75b1e4155520b1da175b686ffc), [`c319edb8e0`](https://github.com/medusajs/medusa/commit/c319edb8e0ecd13d086652147667916e5abab2d8), [`0b9fcb6324`](https://github.com/medusajs/medusa/commit/0b9fcb6324eee9f2556c7e6317775fae93b12a47), [`586df9da25`](https://github.com/medusajs/medusa/commit/586df9da250e492442769f5bac2f8b3de1d46f05), [`b3d826497b`](https://github.com/medusajs/medusa/commit/b3d826497b3dae5e1b26b7924706c24fd5e87ca5), [`a86c87fe14`](https://github.com/medusajs/medusa/commit/a86c87fe1442afce9285e39255914e01012b4449), [`640eccd5dd`](https://github.com/medusajs/medusa/commit/640eccd5ddbb163e0f987ce6c772f1129c2e2632), [`8ea37d03c9`](https://github.com/medusajs/medusa/commit/8ea37d03c914a5004a3e42770668b2d1f7f8f564), [`339a946f38`](https://github.com/medusajs/medusa/commit/339a946f389033c21e05338f9dbf07d88e140533), [`ac829fc67f`](https://github.com/medusajs/medusa/commit/ac829fc67f7495b08f28e55923c59f0fd6320311), [`d9d5afc3cf`](https://github.com/medusajs/medusa/commit/d9d5afc3cfc29221d0e65bff7b78474a8fb8f31f), [`c3c4f49fc2`](https://github.com/medusajs/medusa/commit/c3c4f49fc2126f950e69e291ca939ca88a15afd3), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`0d46abf0ff`](https://github.com/medusajs/medusa/commit/0d46abf0ffa4c5e03bf7d2a9cdf1db828a76bea8), [`fafde4f54d`](https://github.com/medusajs/medusa/commit/fafde4f54d3ef75a7d382e6cbf94e38b3deae99b), [`8dad2b51a2`](https://github.com/medusajs/medusa/commit/8dad2b51a26c4c3c14a6c95f70424c8bef2ad63e), [`0c705d7bd4`](https://github.com/medusajs/medusa/commit/0c705d7bd41a768c48017ae95b3c8414d96c6acb), [`a6d7070dd6`](https://github.com/medusajs/medusa/commit/a6d7070dd669c21ea19d70434d42c2f8167dc309), [`1d91b7429b`](https://github.com/medusajs/medusa/commit/1d91b7429beebd6f09d5027f7f7e1fe74ce3a8ff), [`168f02f138`](https://github.com/medusajs/medusa/commit/168f02f138ad101e1013f2c8c3f8dc19de12accf), [`1ed5f918c3`](https://github.com/medusajs/medusa/commit/1ed5f918c31794a70aca4a4e4cd83cf456593baa), [`c20eb15cd9`](https://github.com/medusajs/medusa/commit/c20eb15cd9b1bd90c8d01f68eca6f0f181cd902d), [`e5945479e0`](https://github.com/medusajs/medusa/commit/e5945479e091d9560ae3e7240306a31031ef4584), [`f5c2256286`](https://github.com/medusajs/medusa/commit/f5c22562867f412040f8bc6c55ab5de3a3735e62), [`000eb61e33`](https://github.com/medusajs/medusa/commit/000eb61e33e0302db95ee6ad1656ea9b430ed471), [`d550be3685`](https://github.com/medusajs/medusa/commit/d550be3685423218d47a20c57a5e06758f4a961a), [`62a7bcc30c`](https://github.com/medusajs/medusa/commit/62a7bcc30cbc7b234b2b51d7858439951a84edeb), [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8), [`6500f18b9b`](https://github.com/medusajs/medusa/commit/6500f18b9b80c5c9c473489e7e740d55dca74303), [`ce39b9b66e`](https://github.com/medusajs/medusa/commit/ce39b9b66e8c277ec0691ea6d0a950003be09cc1), [`a6a4b3f01a`](https://github.com/medusajs/medusa/commit/a6a4b3f01a6d2bd97b1580c59134279a1b033a5d), [`4d51f095b3`](https://github.com/medusajs/medusa/commit/4d51f095b3f98f468cefb760512563f7b77bb9cf), [`4625bd1241`](https://github.com/medusajs/medusa/commit/4625bd12416275b09c22cde4a09cb0f68df5d7c1), [`56b0b45304`](https://github.com/medusajs/medusa/commit/56b0b4530401a6ec5aa155874d371e45bb388fe2), [`cc1b66842c`](https://github.com/medusajs/medusa/commit/cc1b66842cbb37c6eab84e2d8b74844c214f38d7), [`24fb102a56`](https://github.com/medusajs/medusa/commit/24fb102a564b1253d1f8b039bb1e435cc5312fbb), [`e85463b2a7`](https://github.com/medusajs/medusa/commit/e85463b2a717751de2e21c39a4c745449b31affe)]:
- @medusajs/types@1.11.14
- @medusajs/utils@1.11.7
- @medusajs/modules-sdk@1.12.9

View File

@@ -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<IInventoryServiceNext>) => {
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()
})
})
})
},
})

View File

@@ -0,0 +1,13 @@
module.exports = {
transform: {
"^.+\\.[jt]s?$": [
"ts-jest",
{
tsconfig: "tsconfig.json",
isolatedModules: true,
},
],
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
}

View File

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

View File

@@ -0,0 +1,58 @@
{
"name": "@medusajs/inventory-next",
"version": "0.0.3",
"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.15",
"@mikro-orm/cli": "5.9.7",
"cross-env": "^5.2.1",
"jest": "^29.6.3",
"medusa-test-utils": "^1.1.43",
"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.10",
"@medusajs/types": "^1.11.15",
"@medusajs/utils": "^1.11.8",
"@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"
}
}

View File

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

View File

@@ -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_item", "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",
},
},
],
}

View File

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

View File

@@ -0,0 +1,39 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20240307132720 extends Migration {
async up(): Promise<void> {
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<void> {
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;');
}
}

View File

@@ -0,0 +1,3 @@
export * from "./reservation-item"
export * from "./inventory-item"
export * from "./inventory-level"

View File

@@ -0,0 +1,154 @@
import {
BeforeCreate,
Collection,
Entity,
Filter,
Formula,
OneToMany,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
Searchable,
} from "@medusajs/utils"
import { DAL } from "@medusajs/types"
import { InventoryLevel } from "./inventory-level"
import { ReservationItem } from "./reservation-item"
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()
@Searchable()
@Property({ columnType: "text", nullable: true })
sku: string | null = null
@Property({ columnType: "text", nullable: true })
origin_country: string | null = null
@Searchable()
@Property({ columnType: "text", nullable: true })
hs_code: string | null = null
@Searchable()
@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
@Searchable()
@Property({ columnType: "text", nullable: true })
description: string | null = null
@Searchable()
@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<string, unknown> | null = null
@OneToMany(
() => InventoryLevel,
(inventoryLevel) => inventoryLevel.inventory_item,
{
cascade: ["soft-remove" as any],
}
)
location_levels = new Collection<InventoryLevel>(this)
@OneToMany(
() => ReservationItem,
(reservationItem) => reservationItem.inventory_item,
{
cascade: ["soft-remove" as any],
}
)
reservation_items = new Collection<ReservationItem>(this)
@Formula(
(item) =>
`(SELECT SUM(reserved_quantity) FROM inventory_level il WHERE il.inventory_item_id = ${item}.id)`,
{ lazy: true, serializer: Number, hidden: true }
)
reserved_quantity: number
@Formula(
(item) =>
`(SELECT SUM(stocked_quantity) FROM inventory_level il WHERE il.inventory_item_id = ${item}.id)`,
{ lazy: true, serializer: Number, hidden: true }
)
stocked_quantity: number
@BeforeCreate()
private beforeCreate(): void {
this.id = generateEntityId(this.id, "iitem")
}
@OnInit()
private onInit(): void {
this.id = generateEntityId(this.id, "iitem")
}
}

View File

@@ -0,0 +1,114 @@
import {
BeforeCreate,
Entity,
Filter,
ManyToOne,
OnInit,
OnLoad,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { DALUtils, isDefined } 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<string, unknown> | null
@ManyToOne(() => InventoryItem, {
persist: false,
})
inventory_item: InventoryItem
available_quantity: number | null = null
@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")
}
@OnLoad()
private onLoad(): void {
if (isDefined(this.stocked_quantity) && isDefined(this.reserved_quantity)) {
this.available_quantity = this.stocked_quantity - this.reserved_quantity
}
}
}

View File

@@ -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<string, unknown> | 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, "resitem")
}
@OnInit()
private onInit(): void {
this.id = generateEntityId(this.id, "resitem")
}
}

View File

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

View File

@@ -0,0 +1,2 @@
export * from "./inventory-level"
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"

View File

@@ -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<number> {
const manager = super.getActiveManager<SqlEntityManager>(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<number> {
const knex = super.getActiveManager<SqlEntityManager>(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<number> {
const knex = super.getActiveManager<SqlEntityManager>(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)
}
}

View File

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

View File

@@ -0,0 +1,5 @@
describe("noop", function () {
it("should run", function () {
expect(true).toBe(true)
})
})

View File

@@ -0,0 +1,2 @@
export { default as InventoryModuleService } from "./inventory"
export { default as InventoryLevelService } from "./inventory-level"

View File

@@ -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<InjectedDependencies>(
InventoryLevel
)<TEntity> {
protected readonly inventoryLevelRepository: InventoryLevelRepository
constructor(container: InjectedDependencies) {
super(container)
this.inventoryLevelRepository = container.inventoryLevelRepository
}
async retrieveStockedQuantity(
inventoryItemId: string,
locationIds: string[] | string,
context: Context = {}
): Promise<number> {
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<number> {
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
)
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}