feat(medusa, inventory): Inventory Management module (#2956)

* feat: inventory module
This commit is contained in:
Carlos R. L. Rodrigues
2023-01-10 14:38:30 -03:00
committed by GitHub
parent a2df11fc10
commit 93ee248493
22 changed files with 1735 additions and 14 deletions

6
packages/inventory/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,10 @@
src
.turbo
.prettierrc
.env
.babelrc.js
.eslintrc
.gitignore
ormconfig.json
tsconfig.json
jest.config.md

View File

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

View File

@@ -0,0 +1,37 @@
{
"name": "@medusajs/inventory",
"version": "1.0.0",
"description": "Inventory Module for Medusa",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/inventory"
},
"publishConfig": {
"access": "public"
},
"author": "Medusa",
"license": "MIT",
"devDependencies": {
"@medusajs/medusa": "*",
"cross-env": "^5.2.1",
"jest": "^25.5.2",
"ts-jest": "^25.5.1",
"typescript": "^4.4.4"
},
"scripts": {
"watch": "tsc --build --watch",
"prepare": "cross-env NODE_ENV=production yarn run build",
"build": "tsc --build",
"test": "jest --passWithNoTests",
"test:unit": "jest --passWithNoTests"
},
"peerDependencies": {
"@medusajs/medusa": "1.x.x",
"medusa-interfaces": "1.x.x"
},
"dependencies": {
"typeorm": "^0.2.31"
}
}

View File

@@ -0,0 +1 @@
export const CONNECTION_NAME = "inventory_connection"

View File

@@ -0,0 +1,7 @@
import ConnectionLoader from "./loaders/connection"
import InventoryService from "./services/inventory"
import * as SchemaMigration from "./migrations/schema-migrations/1665748086258-inventory_setup"
export const service = InventoryService
export const migrations = [SchemaMigration]
export const loaders = [ConnectionLoader]

View File

@@ -0,0 +1,22 @@
import { ConfigModule } from "@medusajs/medusa"
import { ConnectionOptions, createConnection } from "typeorm"
import { CONNECTION_NAME } from "../config"
import { ReservationItem, InventoryItem, InventoryLevel } from "../models"
export default async ({
configModule,
}: {
configModule: ConfigModule
}): Promise<void> => {
await createConnection({
name: CONNECTION_NAME,
type: configModule.projectConfig.database_type,
url: configModule.projectConfig.database_url,
database: configModule.projectConfig.database_database,
schema: configModule.projectConfig.database_schema,
extra: configModule.projectConfig.database_extra || {},
entities: [ReservationItem, InventoryLevel, InventoryItem],
logging: configModule.projectConfig.database_logging || false,
} as ConnectionOptions)
}

View File

@@ -0,0 +1,126 @@
import { ConfigModule } from "@medusajs/medusa"
import {
createConnection,
ConnectionOptions,
MigrationInterface,
QueryRunner,
} from "typeorm"
import { CONNECTION_NAME } from "../../config"
export const up = async ({ configModule }: { configModule: ConfigModule }) => {
const connection = await createConnection({
name: CONNECTION_NAME,
type: configModule.projectConfig.database_type,
url: configModule.projectConfig.database_url,
extra: configModule.projectConfig.database_extra || {},
schema: configModule.projectConfig.database_schema,
migrations: [inventorySetup1665748086258],
logging: true,
} as ConnectionOptions)
await connection.runMigrations()
await connection.close()
}
export const down = async ({
configModule,
}: {
configModule: ConfigModule
}) => {
const connection = await createConnection({
name: CONNECTION_NAME,
type: configModule.projectConfig.database_type,
url: configModule.projectConfig.database_url,
extra: configModule.projectConfig.database_extra || {},
schema: configModule.projectConfig.database_schema,
migrations: [inventorySetup1665748086258],
logging: true,
} as ConnectionOptions)
await connection.undoLastMigration({ transaction: "all" })
await connection.close()
}
export class inventorySetup1665748086258 implements MigrationInterface {
name = "inventorySetup1665748086258"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "inventory_item" (
"id" character varying NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"deleted_at" TIMESTAMP WITH TIME ZONE,
"sku" text,
"origin_country" text,
"hs_code" text,
"mid_code" text,
"material" text,
"weight" integer,
"length" integer,
"height" integer,
"width" integer,
"requires_shipping" boolean NOT NULL DEFAULT true,
"metadata" jsonb,
CONSTRAINT "PK_inventory_item_id" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "IDX_inventory_item_sku" ON "inventory_item" ("sku");
CREATE TABLE "reservation_item" (
"id" character varying NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"deleted_at" TIMESTAMP WITH TIME ZONE,
"line_item_id" text,
"inventory_item_id" text NOT NULL,
"location_id" text NOT NULL,
"quantity" integer NOT NULL,
"metadata" jsonb,
CONSTRAINT "PK_reservation_item_id" PRIMARY KEY ("id")
);
CREATE INDEX "IDX_reservation_item_line_item_id" ON "reservation_item" ("line_item_id") WHERE deleted_at IS NULL;
CREATE INDEX "IDX_reservation_item_inventory_item_id" ON "reservation_item" ("inventory_item_id") WHERE deleted_at IS NULL;
CREATE INDEX "IDX_reservation_item_location_id" ON "reservation_item" ("location_id") WHERE deleted_at IS NULL;
CREATE TABLE "inventory_level" (
"id" character varying NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"deleted_at" TIMESTAMP WITH TIME ZONE,
"inventory_item_id" text NOT NULL,
"location_id" text NOT NULL,
"stocked_quantity" integer NOT NULL DEFAULT 0,
"reserved_quantity" integer NOT NULL DEFAULT 0,
"incoming_quantity" integer NOT NULL DEFAULT 0,
"metadata" jsonb,
CONSTRAINT "PK_inventory_level_id" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "UQ_inventory_level_inventory_item_id_location_id" ON "inventory_level" ("inventory_item_id", "location_id");
CREATE INDEX "IDX_inventory_level_inventory_item_id" ON "inventory_level" ("inventory_item_id");
CREATE INDEX "IDX_inventory_level_location_id" ON "inventory_level" ("location_id");
`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP INDEX "IDX_inventory_level_location_id";
DROP INDEX "IDX_inventory_level_inventory_item_id";
DROP INDEX "UQ_inventory_level_inventory_item_id_location_id";
DROP TABLE "inventory_level";
DROP INDEX "IDX_reservation_item_location_id";
DROP INDEX "IDX_reservation_item_inventory_item_id";
DROP INDEX "IDX_reservation_item_line_item_id";
DROP TABLE "reservation_item";
DROP INDEX "IDX_inventory_item_sku";
DROP TABLE "inventory_item";
`)
}
}

View File

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

View File

@@ -0,0 +1,44 @@
import { Index, BeforeInsert, Column, Entity } from "typeorm"
import { SoftDeletableEntity, generateEntityId } from "@medusajs/medusa"
@Entity()
export class InventoryItem extends SoftDeletableEntity {
@Index({ unique: true })
@Column({ type: "text", nullable: true })
sku: string | null
@Column({ type: "text", nullable: true })
origin_country: string | null
@Column({ type: "text", nullable: true })
hs_code: string | null
@Column({ type: "text", nullable: true })
mid_code: string | null
@Column({ type: "text", nullable: true })
material: string | null
@Column({ type: "int", nullable: true })
weight: number | null
@Column({ type: "int", nullable: true })
length: number | null
@Column({ type: "int", nullable: true })
height: number | null
@Column({ type: "int", nullable: true })
width: number | null
@Column({ default: true })
requires_shipping: boolean
@Column({ type: "jsonb", nullable: true })
metadata: Record<string, unknown> | null
@BeforeInsert()
private beforeInsert(): void {
this.id = generateEntityId(this.id, "iitem")
}
}

View File

@@ -0,0 +1,31 @@
import { Index, Unique, BeforeInsert, Column, Entity } from "typeorm"
import { SoftDeletableEntity, generateEntityId } from "@medusajs/medusa"
@Entity()
@Index(["inventory_item_id", "location_id"], { unique: true })
export class InventoryLevel extends SoftDeletableEntity {
@Index()
@Column({ type: "text" })
inventory_item_id: string
@Index()
@Column({ type: "text" })
location_id: string
@Column({ default: 0 })
stocked_quantity: number
@Column({ default: 0 })
reserved_quantity: number
@Column({ default: 0 })
incoming_quantity: number
@Column({ type: "jsonb", nullable: true })
metadata: Record<string, unknown> | null
@BeforeInsert()
private beforeInsert(): void {
this.id = generateEntityId(this.id, "ilev")
}
}

View File

@@ -0,0 +1,28 @@
import { Index, Unique, BeforeInsert, Column, Entity } from "typeorm"
import { SoftDeletableEntity, generateEntityId } from "@medusajs/medusa"
@Entity()
export class ReservationItem extends SoftDeletableEntity {
@Index()
@Column({ type: "text", nullable: true })
line_item_id: string | null
@Index()
@Column({ type: "text" })
inventory_item_id: string
@Index()
@Column({ type: "text" })
location_id: string
@Column()
quantity: number
@Column({ type: "jsonb", nullable: true })
metadata: Record<string, unknown> | null
@BeforeInsert()
private beforeInsert(): void {
this.id = generateEntityId(this.id, "resitem")
}
}

View File

@@ -0,0 +1,4 @@
export { default as InventoryLevelService } from "./inventory-level"
export { default as InventoryItemService } from "./inventory-item"
export { default as ReservationItemService } from "./reservation-item"
export { default as InventoryService } from "./inventory"

View File

@@ -0,0 +1,235 @@
import { ILike, In, getConnection, DeepPartial, EntityManager } from "typeorm"
import { isDefined, MedusaError } from "medusa-core-utils"
import {
FindConfig,
buildQuery,
IEventBusService,
FilterableInventoryItemProps,
CreateInventoryItemInput,
} from "@medusajs/medusa"
import { InventoryItem } from "../models"
import { CONNECTION_NAME } from "../config"
type InjectedDependencies = {
eventBusService: IEventBusService
}
export default class InventoryItemService {
static Events = {
CREATED: "inventory-item.created",
UPDATED: "inventory-item.updated",
DELETED: "inventory-item.deleted",
}
protected readonly eventBusService_: IEventBusService
constructor({ eventBusService }: InjectedDependencies) {
this.eventBusService_ = eventBusService
}
private getManager(): EntityManager {
const connection = getConnection(CONNECTION_NAME)
return connection.manager
}
/**
* @param selector - Filter options for inventory items.
* @param config - Configuration for query.
* @return Resolves to the list of inventory items that match the filter.
*/
async list(
selector: FilterableInventoryItemProps = {},
config: FindConfig<InventoryItem> = { relations: [], skip: 0, take: 10 }
): Promise<InventoryItem[]> {
const queryBuilder = this.getListQuery(selector, config)
return await queryBuilder.getMany()
}
private getListQuery(
selector: FilterableInventoryItemProps = {},
config: FindConfig<InventoryItem> = { relations: [], skip: 0, take: 10 }
) {
const manager = this.getManager()
const inventoryItemRepository = manager.getRepository(InventoryItem)
const query = buildQuery(selector, config)
const queryBuilder = inventoryItemRepository.createQueryBuilder("inv_item")
if (query.where.q) {
query.where.sku = ILike(`%${query.where.q as string}%`)
delete query.where.q
}
if ("location_id" in query.where) {
const locationIds = Array.isArray(selector.location_id)
? selector.location_id
: [selector.location_id]
queryBuilder.innerJoin(
"inventory_level",
"level",
"level.inventory_item_id = inv_item.id AND level.location_id IN (:...locationIds)",
{ locationIds }
)
delete query.where.location_id
}
if (query.take) {
queryBuilder.take(query.take)
}
if (query.skip) {
queryBuilder.skip(query.skip)
}
if (query.where) {
queryBuilder.where(query.where)
}
if (query.select) {
queryBuilder.select(query.select.map((s) => "inv_item." + s))
}
if (query.order) {
const toSelect: string[] = []
const parsed = Object.entries(query.order).reduce((acc, [k, v]) => {
const key = `inv_item.${k}`
toSelect.push(key)
acc[key] = v
return acc
}, {})
queryBuilder.addSelect(toSelect)
queryBuilder.orderBy(parsed)
}
return queryBuilder
}
/**
* @param selector - Filter options for inventory items.
* @param config - Configuration for query.
* @return - Resolves to the list of inventory items that match the filter and the count of all matching items.
*/
async listAndCount(
selector: FilterableInventoryItemProps = {},
config: FindConfig<InventoryItem> = { relations: [], skip: 0, take: 10 }
): Promise<[InventoryItem[], number]> {
const queryBuilder = this.getListQuery(selector, config)
return await queryBuilder.getManyAndCount()
}
/**
* Retrieves an inventory item by its id.
* @param inventoryItemId - the id of the inventory item to retrieve.
* @param config - the configuration options for the find operation.
* @return The retrieved inventory item.
* @throws If the inventory item id is not defined or if the inventory item is not found.
*/
async retrieve(
inventoryItemId: string,
config: FindConfig<InventoryItem> = {}
): Promise<InventoryItem> {
if (!isDefined(inventoryItemId)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`"inventoryItemId" must be defined`
)
}
const manager = this.getManager()
const itemRepository = manager.getRepository(InventoryItem)
const query = buildQuery({ id: inventoryItemId }, config)
const [inventoryItem] = await itemRepository.find(query)
if (!inventoryItem) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`InventoryItem with id ${inventoryItemId} was not found`
)
}
return inventoryItem
}
/**
* @param input - Input for creating a new inventory item.
* @return The newly created inventory item.
*/
async create(data: CreateInventoryItemInput): Promise<InventoryItem> {
const manager = this.getManager()
const itemRepository = manager.getRepository(InventoryItem)
const inventoryItem = itemRepository.create({
sku: data.sku,
origin_country: data.origin_country,
metadata: data.metadata,
hs_code: data.hs_code,
mid_code: data.mid_code,
material: data.material,
weight: data.weight,
length: data.length,
height: data.height,
width: data.width,
requires_shipping: data.requires_shipping,
})
const result = await itemRepository.save(inventoryItem)
await this.eventBusService_.emit(InventoryItemService.Events.CREATED, {
id: result.id,
})
return result
}
/**
* @param inventoryItemId - The id of the inventory item to update.
* @param update - The updates to apply to the inventory item.
* @return The updated inventory item.
*/
async update(
inventoryItemId: string,
data: Omit<
DeepPartial<InventoryItem>,
"id" | "created_at" | "metadata" | "deleted_at"
>
): Promise<InventoryItem> {
const manager = this.getManager()
const itemRepository = manager.getRepository(InventoryItem)
const item = await this.retrieve(inventoryItemId)
const shouldUpdate = Object.keys(data).some((key) => {
return item[key] !== data[key]
})
if (shouldUpdate) {
itemRepository.merge(item, data)
await itemRepository.save(item)
await this.eventBusService_.emit(InventoryItemService.Events.UPDATED, {
id: item.id,
})
}
return item
}
/**
* @param inventoryItemId - The id of the inventory item to delete.
*/
async delete(inventoryItemId: string): Promise<void> {
const manager = this.getManager()
const itemRepository = manager.getRepository(InventoryItem)
await itemRepository.softRemove({ id: inventoryItemId })
await this.eventBusService_.emit(InventoryItemService.Events.DELETED, {
id: inventoryItemId,
})
}
}

View File

@@ -0,0 +1,287 @@
import { getConnection, DeepPartial, EntityManager } from "typeorm"
import { isDefined, MedusaError } from "medusa-core-utils"
import {
FindConfig,
buildQuery,
FilterableInventoryLevelProps,
CreateInventoryLevelInput,
IEventBusService,
TransactionBaseService,
} from "@medusajs/medusa"
import { InventoryLevel } from "../models"
import { CONNECTION_NAME } from "../config"
type InjectedDependencies = {
eventBusService: IEventBusService
}
export default class InventoryLevelService extends TransactionBaseService {
static Events = {
CREATED: "inventory-level.created",
UPDATED: "inventory-level.updated",
DELETED: "inventory-level.deleted",
}
protected manager_: EntityManager
protected transactionManager_: EntityManager | undefined
protected readonly eventBusService_: IEventBusService
constructor({ eventBusService }: InjectedDependencies) {
super(arguments[0])
this.eventBusService_ = eventBusService
this.manager_ = this.getManager()
}
private getManager(): EntityManager {
if (this.manager_) {
return this.transactionManager_ ?? this.manager_
}
const connection = getConnection(CONNECTION_NAME)
return connection.manager
}
/**
* Retrieves a list of inventory levels based on the provided selector and configuration.
* @param selector - An object containing filterable properties for inventory levels.
* @param config - An object containing configuration options for the query.
* @return Array of inventory levels.
*/
async list(
selector: FilterableInventoryLevelProps = {},
config: FindConfig<InventoryLevel> = { relations: [], skip: 0, take: 10 }
): Promise<InventoryLevel[]> {
const manager = this.getManager()
const levelRepository = manager.getRepository(InventoryLevel)
const query = buildQuery(selector, config)
return await levelRepository.find(query)
}
/**
* Retrieves a list of inventory levels and a count based on the provided selector and configuration.
* @param selector - An object containing filterable properties for inventory levels.
* @param config - An object containing configuration options for the query.
* @return An array of inventory levels and a count.
*/
async listAndCount(
selector: FilterableInventoryLevelProps = {},
config: FindConfig<InventoryLevel> = { relations: [], skip: 0, take: 10 }
): Promise<[InventoryLevel[], number]> {
const manager = this.getManager()
const levelRepository = manager.getRepository(InventoryLevel)
const query = buildQuery(selector, config)
return await levelRepository.findAndCount(query)
}
/**
* Retrieves a single inventory level by its ID.
* @param inventoryLevelId - The ID of the inventory level to retrieve.
* @param config - An object containing configuration options for the query.
* @return A inventory level.
* @throws If the inventory level ID is not defined or the given ID was not found.
*/
async retrieve(
inventoryLevelId: string,
config: FindConfig<InventoryLevel> = {}
): Promise<InventoryLevel> {
if (!isDefined(inventoryLevelId)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`"inventoryLevelId" must be defined`
)
}
const manager = this.getManager()
const levelRepository = manager.getRepository(InventoryLevel)
const query = buildQuery({ id: inventoryLevelId }, config)
const [inventoryLevel] = await levelRepository.find(query)
if (!inventoryLevel) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`InventoryLevel with id ${inventoryLevelId} was not found`
)
}
return inventoryLevel
}
/**
* Creates a new inventory level.
* @param data - An object containing the properties for the new inventory level.
* @return The created inventory level.
*/
async create(data: CreateInventoryLevelInput): Promise<InventoryLevel> {
const result = await this.atomicPhase_(async (manager) => {
const levelRepository = manager.getRepository(InventoryLevel)
const inventoryLevel = levelRepository.create({
location_id: data.location_id,
inventory_item_id: data.inventory_item_id,
stocked_quantity: data.stocked_quantity,
reserved_quantity: data.reserved_quantity,
incoming_quantity: data.incoming_quantity,
})
return await levelRepository.save(inventoryLevel)
})
await this.eventBusService_.emit(InventoryLevelService.Events.CREATED, {
id: result.id,
})
return result
}
/**
* Updates an existing inventory level.
* @param inventoryLevelId - The ID of the inventory level to update.
* @param data - An object containing the properties to update on the inventory level.
* @param autoSave - A flag indicating whether to save the changes automatically.
* @return The updated inventory level.
* @throws If the inventory level ID is not defined or the given ID was not found.
*/
async update(
inventoryLevelId: string,
data: Omit<
DeepPartial<InventoryLevel>,
"id" | "created_at" | "metadata" | "deleted_at"
>
): Promise<InventoryLevel> {
return await this.atomicPhase_(async (manager) => {
const levelRepository = manager.getRepository(InventoryLevel)
const item = await this.retrieve(inventoryLevelId)
const shouldUpdate = Object.keys(data).some((key) => {
return item[key] !== data[key]
})
if (shouldUpdate) {
levelRepository.merge(item, data)
await levelRepository.save(item)
await this.eventBusService_.emit(InventoryLevelService.Events.UPDATED, {
id: item.id,
})
}
return item
})
}
/**
* Adjust the reserved quantity for an inventory item at a specific location.
* @param inventoryItemId - The ID of the inventory item.
* @param locationId - The ID of the location.
* @param quantity - The quantity to adjust from the reserved quantity.
*/
async adjustReservedQuantity(
inventoryItemId: string,
locationId: string,
quantity: number
): Promise<void> {
await this.atomicPhase_(async (manager) => {
await manager
.createQueryBuilder()
.update(InventoryLevel)
.set({ reserved_quantity: () => `reserved_quantity + ${quantity}` })
.where(
"inventory_item_id = :inventoryItemId AND location_id = :locationId",
{ inventoryItemId, locationId }
)
.execute()
})
}
/**
* Deletes an inventory level by ID.
* @param inventoryLevelId - The ID of the inventory level to delete.
*/
async delete(inventoryLevelId: string): Promise<void> {
await this.atomicPhase_(async (manager) => {
const levelRepository = manager.getRepository(InventoryLevel)
await levelRepository.delete({ id: inventoryLevelId })
})
await this.eventBusService_.emit(InventoryLevelService.Events.DELETED, {
id: inventoryLevelId,
})
}
/**
* Gets the total stocked quantity for a specific inventory item at multiple locations.
* @param inventoryItemId - The ID of the inventory item.
* @param locationIds - The IDs of the locations.
* @return The total stocked quantity.
*/
async getStockedQuantity(
inventoryItemId: string,
locationIds: string[]
): Promise<number> {
const manager = this.getManager()
const levelRepository = manager.getRepository(InventoryLevel)
const result = await levelRepository
.createQueryBuilder()
.select("SUM(stocked_quantity)", "quantity")
.where("inventory_item_id = :inventoryItemId", { inventoryItemId })
.andWhere("location_id IN (:...locationIds)", { locationIds })
.getRawOne()
return result.quantity
}
/**
* Gets the total available quantity for a specific inventory item at multiple locations.
* @param inventoryItemId - The ID of the inventory item.
* @param locationIds - The IDs of the locations.
* @return The total available quantity.
*/
async getAvailableQuantity(
inventoryItemId: string,
locationIds: string[]
): Promise<number> {
const manager = this.getManager()
const levelRepository = manager.getRepository(InventoryLevel)
const result = await levelRepository
.createQueryBuilder()
.select("SUM(stocked_quantity - reserved_quantity)", "quantity")
.where("inventory_item_id = :inventoryItemId", { inventoryItemId })
.andWhere("location_id IN (:...locationIds)", { locationIds })
.getRawOne()
return result.quantity
}
/**
* Gets the total reserved quantity for a specific inventory item at multiple locations.
* @param inventoryItemId - The ID of the inventory item.
* @param locationIds - The IDs of the locations.
* @return The total reserved quantity.
*/
async getReservedQuantity(
inventoryItemId: string,
locationIds: string[]
): Promise<number> {
const manager = this.getManager()
const levelRepository = manager.getRepository(InventoryLevel)
const result = await levelRepository
.createQueryBuilder()
.select("SUM(reserved_quantity)", "quantity")
.where("inventory_item_id = :inventoryItemId", { inventoryItemId })
.andWhere("location_id IN (:...locationIds)", { locationIds })
.getRawOne()
return result.quantity
}
}

View File

@@ -0,0 +1,412 @@
import { MedusaError } from "medusa-core-utils"
import {
FindConfig,
IInventoryService,
FilterableInventoryItemProps,
FilterableReservationItemProps,
CreateInventoryItemInput,
CreateReservationItemInput,
FilterableInventoryLevelProps,
CreateInventoryLevelInput,
UpdateInventoryLevelInput,
UpdateReservationItemInput,
IEventBusService,
InventoryItemDTO,
ReservationItemDTO,
InventoryLevelDTO,
} from "@medusajs/medusa"
import {
InventoryItemService,
ReservationItemService,
InventoryLevelService,
} from "./"
type InjectedDependencies = {
eventBusService: IEventBusService
}
export default class InventoryService implements IInventoryService {
protected readonly eventBusService_: IEventBusService
protected readonly inventoryItemService_: InventoryItemService
protected readonly reservationItemService_: ReservationItemService
protected readonly inventoryLevelService_: InventoryLevelService
constructor({ eventBusService }: InjectedDependencies) {
this.eventBusService_ = eventBusService
this.inventoryItemService_ = new InventoryItemService({ eventBusService })
this.inventoryLevelService_ = new InventoryLevelService({
eventBusService,
})
this.reservationItemService_ = new ReservationItemService({
eventBusService,
inventoryLevelService: this.inventoryLevelService_,
})
}
/**
* Lists inventory items that match the given selector
* @param selector - the selector to filter inventory items by
* @param config - the find configuration to use
* @return A tuple of inventory items and their total count
*/
async listInventoryItems(
selector: FilterableInventoryItemProps,
config: FindConfig<InventoryItemDTO> = { relations: [], skip: 0, take: 10 }
): Promise<[InventoryItemDTO[], number]> {
return await this.inventoryItemService_.listAndCount(selector, config)
}
/**
* Lists inventory levels that match the given selector
* @param selector - the selector to filter inventory levels by
* @param config - the find configuration to use
* @return A tuple of inventory levels and their total count
*/
async listInventoryLevels(
selector: FilterableInventoryLevelProps,
config: FindConfig<InventoryLevelDTO> = { relations: [], skip: 0, take: 10 }
): Promise<[InventoryLevelDTO[], number]> {
return await this.inventoryLevelService_.listAndCount(selector, config)
}
/**
* Lists reservation items that match the given selector
* @param selector - the selector to filter reservation items by
* @param config - the find configuration to use
* @return A tuple of reservation items and their total count
*/
async listReservationItems(
selector: FilterableReservationItemProps,
config: FindConfig<ReservationItemDTO> = {
relations: [],
skip: 0,
take: 10,
}
): Promise<[ReservationItemDTO[], number]> {
return await this.reservationItemService_.listAndCount(selector, config)
}
/**
* Retrieves an inventory item with the given id
* @param inventoryItemId - the id of the inventory item to retrieve
* @param config - the find configuration to use
* @return The retrieved inventory item
*/
async retrieveInventoryItem(
inventoryItemId: string,
config?: FindConfig<InventoryItemDTO>
): Promise<InventoryItemDTO> {
const inventoryItem = await this.inventoryItemService_.retrieve(
inventoryItemId,
config
)
return { ...inventoryItem }
}
/**
* Retrieves an inventory level for a given inventory item and location
* @param inventoryItemId - the id of the inventory item
* @param locationId - the id of the location
* @return the retrieved inventory level
*/
async retrieveInventoryLevel(
inventoryItemId: string,
locationId: string
): Promise<InventoryLevelDTO> {
const [inventoryLevel] = await this.inventoryLevelService_.list(
{ inventory_item_id: inventoryItemId, location_id: locationId },
{ take: 1 }
)
if (!inventoryLevel) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Inventory level for item ${inventoryItemId} and location ${locationId} not found`
)
}
return inventoryLevel
}
/**
* Creates a reservation item
* @param input - the input object
* @return The created reservation item
*/
async createReservationItem(
input: CreateReservationItemInput
): Promise<ReservationItemDTO> {
// Verify that the item is stocked at the location
const [inventoryLevel] = await this.inventoryLevelService_.list(
{
inventory_item_id: input.inventory_item_id,
location_id: input.location_id,
},
{ take: 1 }
)
if (!inventoryLevel) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Item ${input.inventory_item_id} is not stocked at location ${input.location_id}`
)
}
const reservationItem = await this.reservationItemService_.create(input)
return { ...reservationItem }
}
/**
* Creates an inventory item
* @param input - the input object
* @return The created inventory item
*/
async createInventoryItem(
input: CreateInventoryItemInput
): Promise<InventoryItemDTO> {
const inventoryItem = await this.inventoryItemService_.create(input)
return { ...inventoryItem }
}
/**
* Creates an inventory item
* @param input - the input object
* @return The created inventory level
*/
async createInventoryLevel(
input: CreateInventoryLevelInput
): Promise<InventoryLevelDTO> {
return await this.inventoryLevelService_.create(input)
}
/**
* Updates an inventory item
* @param inventoryItemId - the id of the inventory item to update
* @param input - the input object
* @return The updated inventory item
*/
async updateInventoryItem(
inventoryItemId: string,
input: Partial<CreateInventoryItemInput>
): Promise<InventoryItemDTO> {
const inventoryItem = await this.inventoryItemService_.update(
inventoryItemId,
input
)
return { ...inventoryItem }
}
/**
* Deletes an inventory item
* @param inventoryItemId - the id of the inventory item to delete
*/
async deleteInventoryItem(inventoryItemId: string): Promise<void> {
return await this.inventoryItemService_.delete(inventoryItemId)
}
/**
* 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
*/
async deleteInventoryLevel(
inventoryItemId: string,
locationId: string
): Promise<void> {
const [inventoryLevel] = await this.inventoryLevelService_.list(
{ inventory_item_id: inventoryItemId, location_id: locationId },
{ take: 1 }
)
if (!inventoryLevel) {
return
}
return await this.inventoryLevelService_.delete(inventoryLevel.id)
}
/**
* Updates 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 input - the input object
* @return The updated inventory level
*/
async updateInventoryLevel(
inventoryItemId: string,
locationId: string,
input: UpdateInventoryLevelInput
): Promise<InventoryLevelDTO> {
const [inventoryLevel] = await this.inventoryLevelService_.list(
{ inventory_item_id: inventoryItemId, location_id: locationId },
{ take: 1 }
)
if (!inventoryLevel) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Inventory level for item ${inventoryItemId} and location ${locationId} not found`
)
}
return await this.inventoryLevelService_.update(inventoryLevel.id, input)
}
/**
* Updates a reservation item
* @param inventoryItemId - the id of the inventory item associated with the level
* @param input - the input object
* @return The updated inventory level
*/
async updateReservationItem(
reservationItemId: string,
input: UpdateReservationItemInput
): Promise<ReservationItemDTO> {
return await this.reservationItemService_.update(reservationItemId, input)
}
/**
* Deletes reservation items by line item
* @param lineItemId - the id of the line item associated with the reservation item
*/
async deleteReservationItemsByLineItem(lineItemId: string): Promise<void> {
return await this.reservationItemService_.deleteByLineItem(lineItemId)
}
/**
* Deletes a reservation item
* @param reservationItemId - the id of the reservation item to delete
*/
async deleteReservationItem(reservationItemId: string): Promise<void> {
return await this.reservationItemService_.delete(reservationItemId)
}
/**
* 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)
* @return The updated inventory level
* @throws when the inventory level is not found
*/
async adjustInventory(
inventoryItemId: string,
locationId: string,
adjustment: number
): Promise<InventoryLevelDTO> {
const [inventoryLevel] = await this.inventoryLevelService_.list(
{ inventory_item_id: inventoryItemId, location_id: locationId },
{ take: 1 }
)
if (!inventoryLevel) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Inventory level for inventory item ${inventoryItemId} and location ${locationId} not found`
)
}
const updatedInventoryLevel = await this.inventoryLevelService_.update(
inventoryLevel.id,
{
stocked_quantity: inventoryLevel.stocked_quantity + adjustment,
}
)
return { ...updatedInventoryLevel }
}
/**
* 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
* @return The available quantity
* @throws when the inventory item is not found
*/
async retrieveAvailableQuantity(
inventoryItemId: string,
locationIds: string[]
): Promise<number> {
// Throws if item does not exist
await this.inventoryItemService_.retrieve(inventoryItemId, {
select: ["id"],
})
const availableQuantity =
await this.inventoryLevelService_.getAvailableQuantity(
inventoryItemId,
locationIds
)
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
* @return The stocked quantity
* @throws when the inventory item is not found
*/
async retrieveStockedQuantity(
inventoryItemId: string,
locationIds: string[]
): Promise<number> {
// Throws if item does not exist
await this.inventoryItemService_.retrieve(inventoryItemId, {
select: ["id"],
})
const stockedQuantity =
await this.inventoryLevelService_.getStockedQuantity(
inventoryItemId,
locationIds
)
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
* @return The reserved quantity
* @throws when the inventory item is not found
*/
async retrieveReservedQuantity(
inventoryItemId: string,
locationIds: string[]
): Promise<number> {
// Throws if item does not exist
await this.inventoryItemService_.retrieve(inventoryItemId, {
select: ["id"],
})
const reservedQuantity =
await this.inventoryLevelService_.getReservedQuantity(
inventoryItemId,
locationIds
)
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
* @return Whether there is sufficient inventory
*/
async confirmInventory(
inventoryItemId: string,
locationIds: string[],
quantity: number
): Promise<boolean> {
const availableQuantity = await this.retrieveAvailableQuantity(
inventoryItemId,
locationIds
)
return availableQuantity >= quantity
}
}

View File

@@ -0,0 +1,268 @@
import { getConnection, DeepPartial, EntityManager } from "typeorm"
import { isDefined, MedusaError } from "medusa-core-utils"
import {
FindConfig,
buildQuery,
IEventBusService,
FilterableReservationItemProps,
CreateReservationItemInput,
TransactionBaseService,
UpdateReservationItemInput,
} from "@medusajs/medusa"
import { ReservationItem } from "../models"
import { CONNECTION_NAME } from "../config"
import { InventoryLevelService } from "."
type InjectedDependencies = {
eventBusService: IEventBusService
inventoryLevelService: InventoryLevelService
}
export default class ReservationItemService extends TransactionBaseService {
static Events = {
CREATED: "reservation-item.created",
UPDATED: "reservation-item.updated",
DELETED: "reservation-item.deleted",
DELETED_BY_LINE_ITEM: "reservation-item.deleted-by-line-item",
}
protected manager_: EntityManager
protected transactionManager_: EntityManager | undefined
protected readonly eventBusService_: IEventBusService
protected readonly inventoryLevelService_: InventoryLevelService
constructor({
eventBusService,
inventoryLevelService,
}: InjectedDependencies) {
super(arguments[0])
this.manager_ = this.getManager()
this.eventBusService_ = eventBusService
this.inventoryLevelService_ = inventoryLevelService
}
private getManager(): EntityManager {
if (this.manager_) {
return this.transactionManager_ ?? this.manager_
}
const connection = getConnection(CONNECTION_NAME)
return connection.manager
}
/**
* Lists reservation items that match the provided filter.
* @param selector - Filters to apply to the reservation items.
* @param config - Configuration for the query.
* @return Array of reservation items that match the selector.
*/
async list(
selector: FilterableReservationItemProps = {},
config: FindConfig<ReservationItem> = { relations: [], skip: 0, take: 10 }
): Promise<ReservationItem[]> {
const manager = this.getManager()
const itemRepository = manager.getRepository(ReservationItem)
const query = buildQuery(selector, config)
return await itemRepository.find(query)
}
/**
* Lists reservation items that match the provided filter and returns the total count.
* @param selector - Filters to apply to the reservation items.
* @param config - Configuration for the query.
* @return Array of reservation items that match the selector and the total count.
*/
async listAndCount(
selector: FilterableReservationItemProps = {},
config: FindConfig<ReservationItem> = { relations: [], skip: 0, take: 10 }
): Promise<[ReservationItem[], number]> {
const manager = this.getManager()
const itemRepository = manager.getRepository(ReservationItem)
const query = buildQuery(selector, config)
return await itemRepository.findAndCount(query)
}
/**
* Retrieves a reservation item by its id.
* @param reservationItemId - The id of the reservation item to retrieve.
* @param config - Configuration for the query.
* @return The reservation item with the provided id.
* @throws If reservationItemId is not defined or if the reservation item was not found.
*/
async retrieve(
reservationItemId: string,
config: FindConfig<ReservationItem> = {}
): Promise<ReservationItem> {
if (!isDefined(reservationItemId)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`"reservationItemId" must be defined`
)
}
const manager = this.getManager()
const reservationItemRepository = manager.getRepository(ReservationItem)
const query = buildQuery({ id: reservationItemId }, config)
const [reservationItem] = await reservationItemRepository.find(query)
if (!reservationItem) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`ReservationItem with id ${reservationItemId} was not found`
)
}
return reservationItem
}
/**
* Create a new reservation item.
* @param data - The reservation item data.
* @return The created reservation item.
*/
async create(data: CreateReservationItemInput): Promise<ReservationItem> {
const result = await this.atomicPhase_(async (manager) => {
const itemRepository = manager.getRepository(ReservationItem)
const inventoryItem = itemRepository.create({
inventory_item_id: data.inventory_item_id,
line_item_id: data.line_item_id,
location_id: data.location_id,
quantity: data.quantity,
metadata: data.metadata,
})
const [newInventoryItem] = await Promise.all([
itemRepository.save(inventoryItem),
this.inventoryLevelService_
.withTransaction(manager)
.adjustReservedQuantity(
data.inventory_item_id,
data.location_id,
data.quantity
),
])
return newInventoryItem
})
await this.eventBusService_.emit(ReservationItemService.Events.CREATED, {
id: result.id,
})
return result
}
/**
* Update a reservation item.
* @param reservationItemId - The reservation item's id.
* @param data - The reservation item data to update.
* @return The updated reservation item.
*/
async update(
reservationItemId: string,
data: UpdateReservationItemInput
): Promise<ReservationItem> {
const updatedItem = await this.atomicPhase_(async (manager) => {
const itemRepository = manager.getRepository(ReservationItem)
const item = await this.retrieve(reservationItemId)
const shouldUpdateQuantity =
isDefined(data.quantity) && data.quantity !== item.quantity
const ops: Promise<unknown>[] = []
if (shouldUpdateQuantity) {
const quantityDiff = data.quantity! - item.quantity
ops.push(
this.inventoryLevelService_
.withTransaction(manager)
.adjustReservedQuantity(
item.inventory_item_id,
item.location_id,
quantityDiff
)
)
}
const mergedItem = itemRepository.merge(item, data)
ops.push(itemRepository.save(item))
await Promise.all(ops)
return mergedItem
})
await this.eventBusService_.emit(ReservationItemService.Events.UPDATED, {
id: updatedItem.id,
})
return updatedItem
}
/**
* Deletes a reservation item by line item id.
* @param lineItemId - the id of the line item to delete.
*/
async deleteByLineItem(lineItemId: string): Promise<void> {
await this.atomicPhase_(async (manager) => {
const itemRepository = manager.getRepository(ReservationItem)
const items = await this.list({ line_item_id: lineItemId })
const ops: Promise<unknown>[] = []
for (const item of items) {
ops.push(itemRepository.softRemove({ line_item_id: lineItemId }))
ops.push(
this.inventoryLevelService_
.withTransaction(manager)
.adjustReservedQuantity(
item.inventory_item_id,
item.location_id,
item.quantity * -1
)
)
}
await Promise.all(ops)
})
await this.eventBusService_.emit(
ReservationItemService.Events.DELETED_BY_LINE_ITEM,
{
line_item_id: lineItemId,
}
)
}
/**
* Deletes a reservation item by id.
* @param reservationItemId - the id of the reservation item to delete.
*/
async delete(reservationItemId: string): Promise<void> {
await this.atomicPhase_(async (manager) => {
const itemRepository = manager.getRepository(ReservationItem)
const item = await this.retrieve(reservationItemId)
await Promise.all([
itemRepository.softRemove({ id: reservationItemId }),
this.inventoryLevelService_
.withTransaction(manager)
.adjustReservedQuantity(
item.inventory_item_id,
item.location_id,
item.quantity * -1
),
])
})
await this.eventBusService_.emit(ReservationItemService.Events.DELETED, {
id: reservationItemId,
})
}
}

View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"lib": [
"es5",
"es6",
"es2019"
],
"target": "es5",
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true // to use ES5 specific tooling
},
"include": ["./src/**/*", "index.d.ts"],
"exclude": [
"./dist/**/*",
"./src/**/__tests__",
"./src/**/__mocks__",
"node_modules"
]
}

View File

@@ -31,12 +31,12 @@ export interface IInventoryService {
): Promise<[InventoryLevelDTO[], number]>
retrieveInventoryItem(
itemId: string,
inventoryItemId: string,
config?: FindConfig<InventoryItemDTO>
): Promise<InventoryItemDTO>
retrieveInventoryLevel(
itemId: string,
inventoryItemId: string,
locationId: string
): Promise<InventoryLevelDTO>
@@ -53,48 +53,56 @@ export interface IInventoryService {
): Promise<InventoryLevelDTO>
updateInventoryLevel(
itemId: string,
inventoryItemId: string,
locationId: string,
update: UpdateInventoryLevelInput
): Promise<InventoryLevelDTO>
updateInventoryItem(
itemId: string,
inventoryItemId: string,
input: CreateInventoryItemInput
): Promise<InventoryItemDTO>
updateReservationItem(
reservationId: string,
update: UpdateReservationItemInput
reservationItemId: string,
input: UpdateReservationItemInput
): Promise<ReservationItemDTO>
deleteReservationItemsByLineItem(lineItemId: string): Promise<void>
deleteReservationItem(id: string): Promise<void>
deleteReservationItem(reservationItemId: string): Promise<void>
deleteInventoryItem(itemId: string): Promise<void>
deleteInventoryItem(inventoryItemId: string): Promise<void>
deleteInventoryLevel(itemId: string, locationId: string): Promise<void>
deleteInventoryLevel(
inventoryLevelId: string,
locationId: string
): Promise<void>
adjustInventory(
itemId: string,
inventoryItemId: string,
locationId: string,
adjustment: number
): Promise<InventoryLevelDTO>
confirmInventory(
itemId: string,
inventoryItemId: string,
locationIds: string[],
quantity: number
): Promise<boolean>
retrieveAvailableQuantity(
itemId: string,
inventoryItemId: string,
locationIds: string[]
): Promise<number>
retrieveStockedQuantity(
itemId: string,
inventoryItemId: string,
locationIds: string[]
): Promise<number>
retrieveReservedQuantity(
inventoryItemId: string,
locationIds: string[]
): Promise<number>
}

View File

@@ -0,0 +1,128 @@
import dotenv from "dotenv"
import { AwilixContainer } from "awilix"
import express from "express"
import {
ProductVariantService,
ProductVariantInventoryService,
} from "../services"
import { ProductVariant } from "../models"
import { IInventoryService, IStockLocationService } from "../interfaces"
import loaders from "../loaders"
dotenv.config()
const BATCH_SIZE = 100
const migrateProductVariant = async (
variant: ProductVariant,
locationId: string,
{ container }: { container: AwilixContainer }
) => {
const productVariantInventoryService: ProductVariantInventoryService =
container.resolve("productVariantInventoryService")
const inventoryService: IInventoryService =
container.resolve("inventoryService")
if (!variant.manage_inventory) {
return
}
const inventoryItem = await inventoryService.createInventoryItem({
sku: variant.sku,
material: variant.material,
width: variant.width,
length: variant.length,
height: variant.height,
weight: variant.weight,
origin_country: variant.origin_country,
hs_code: variant.hs_code,
mid_code: variant.mid_code,
requires_shipping: true,
})
await productVariantInventoryService.attachInventoryItem(
variant.id,
inventoryItem.id,
1
)
await inventoryService.createInventoryLevel({
location_id: locationId,
inventory_item_id: inventoryItem.id,
stocked_quantity: variant.inventory_quantity,
incoming_quantity: 0,
})
}
const migrateStockLocation = async (container: AwilixContainer) => {
const stockLocationService: IStockLocationService = container.resolve(
"stockLocationService"
)
const existing = await stockLocationService.list({}, { take: 1 })
if (existing.length) {
return existing[0].id
}
const stockLocation = await stockLocationService.create({ name: "Default" })
return stockLocation.id
}
const processBatch = async (
variants: ProductVariant[],
locationId: string,
container: AwilixContainer
) => {
await Promise.all(
variants.map(async (variant) => {
await migrateProductVariant(variant, locationId, { container })
})
)
}
const migrate = async function ({ directory }) {
const app = express()
const { container } = await loaders({
directory,
expressApp: app,
isTest: false,
})
const variantService: ProductVariantService = await container.resolve(
"productVariantService"
)
const defaultLocationId = await migrateStockLocation(container)
const [variants, totalCount] = await variantService.listAndCount(
{},
{ take: BATCH_SIZE, order: { id: "ASC" } }
)
await processBatch(variants, defaultLocationId, container)
let processedCount = variants.length
console.log(`Processed ${processedCount} of ${totalCount}`)
while (processedCount < totalCount) {
const nextBatch = await variantService.list(
{},
{
skip: processedCount,
take: BATCH_SIZE,
order: { id: "ASC" },
}
)
await processBatch(nextBatch, defaultLocationId, container)
processedCount += nextBatch.length
console.log(`Processed ${processedCount} of ${totalCount}`)
}
console.log("Done")
process.exit(0)
}
migrate({ directory: process.cwd() })

View File

@@ -34,6 +34,7 @@ export type InventoryLevelDTO = {
inventory_item_id: string
location_id: string
stocked_quantity: number
reserved_quantity: number
incoming_quantity: number
metadata: Record<string, unknown> | null
created_at: string | Date
@@ -87,6 +88,7 @@ export type FilterableInventoryLevelProps = {
inventory_item_id?: string | string[]
location_id?: string | string[]
stocked_quantity?: number | NumericalComparisonOperator
reserved_quantity?: number | NumericalComparisonOperator
incoming_quantity?: number | NumericalComparisonOperator
}
@@ -94,7 +96,8 @@ export type CreateInventoryLevelInput = {
inventory_item_id: string
location_id: string
stocked_quantity: number
incoming_quantity: number
reserved_quantity?: number
incoming_quantity?: number
}
export type UpdateInventoryLevelInput = {

View File

@@ -4332,6 +4332,22 @@ __metadata:
languageName: node
linkType: hard
"@medusajs/inventory@workspace:packages/inventory":
version: 0.0.0-use.local
resolution: "@medusajs/inventory@workspace:packages/inventory"
dependencies:
"@medusajs/medusa": "*"
cross-env: ^5.2.1
jest: ^25.5.2
ts-jest: ^25.5.1
typeorm: ^0.2.31
typescript: ^4.4.4
peerDependencies:
"@medusajs/medusa": 1.x.x
medusa-interfaces: 1.x.x
languageName: unknown
linkType: soft
"@medusajs/medusa-cli@^1.3.5, @medusajs/medusa-cli@workspace:packages/medusa-cli":
version: 0.0.0-use.local
resolution: "@medusajs/medusa-cli@workspace:packages/medusa-cli"