feat(medusa, inventory): Inventory Management module (#2956)
* feat: inventory module
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
/dist
|
||||||
|
node_modules
|
||||||
|
.DS_store
|
||||||
|
.env*
|
||||||
|
.env
|
||||||
|
*.sql
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
src
|
||||||
|
.turbo
|
||||||
|
.prettierrc
|
||||||
|
.env
|
||||||
|
.babelrc.js
|
||||||
|
.eslintrc
|
||||||
|
.gitignore
|
||||||
|
ormconfig.json
|
||||||
|
tsconfig.json
|
||||||
|
jest.config.md
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
globals: {
|
||||||
|
"ts-jest": {
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
"^.+\\.[jt]s?$": "ts-jest",
|
||||||
|
},
|
||||||
|
testEnvironment: `node`,
|
||||||
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const CONNECTION_NAME = "inventory_connection"
|
||||||
@@ -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]
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./reservation-item"
|
||||||
|
export * from "./inventory-item"
|
||||||
|
export * from "./inventory-level"
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -31,12 +31,12 @@ export interface IInventoryService {
|
|||||||
): Promise<[InventoryLevelDTO[], number]>
|
): Promise<[InventoryLevelDTO[], number]>
|
||||||
|
|
||||||
retrieveInventoryItem(
|
retrieveInventoryItem(
|
||||||
itemId: string,
|
inventoryItemId: string,
|
||||||
config?: FindConfig<InventoryItemDTO>
|
config?: FindConfig<InventoryItemDTO>
|
||||||
): Promise<InventoryItemDTO>
|
): Promise<InventoryItemDTO>
|
||||||
|
|
||||||
retrieveInventoryLevel(
|
retrieveInventoryLevel(
|
||||||
itemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string
|
locationId: string
|
||||||
): Promise<InventoryLevelDTO>
|
): Promise<InventoryLevelDTO>
|
||||||
|
|
||||||
@@ -53,48 +53,56 @@ export interface IInventoryService {
|
|||||||
): Promise<InventoryLevelDTO>
|
): Promise<InventoryLevelDTO>
|
||||||
|
|
||||||
updateInventoryLevel(
|
updateInventoryLevel(
|
||||||
itemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string,
|
locationId: string,
|
||||||
update: UpdateInventoryLevelInput
|
update: UpdateInventoryLevelInput
|
||||||
): Promise<InventoryLevelDTO>
|
): Promise<InventoryLevelDTO>
|
||||||
|
|
||||||
updateInventoryItem(
|
updateInventoryItem(
|
||||||
itemId: string,
|
inventoryItemId: string,
|
||||||
input: CreateInventoryItemInput
|
input: CreateInventoryItemInput
|
||||||
): Promise<InventoryItemDTO>
|
): Promise<InventoryItemDTO>
|
||||||
|
|
||||||
updateReservationItem(
|
updateReservationItem(
|
||||||
reservationId: string,
|
reservationItemId: string,
|
||||||
update: UpdateReservationItemInput
|
input: UpdateReservationItemInput
|
||||||
): Promise<ReservationItemDTO>
|
): Promise<ReservationItemDTO>
|
||||||
|
|
||||||
deleteReservationItemsByLineItem(lineItemId: string): Promise<void>
|
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(
|
adjustInventory(
|
||||||
itemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string,
|
locationId: string,
|
||||||
adjustment: number
|
adjustment: number
|
||||||
): Promise<InventoryLevelDTO>
|
): Promise<InventoryLevelDTO>
|
||||||
|
|
||||||
confirmInventory(
|
confirmInventory(
|
||||||
itemId: string,
|
inventoryItemId: string,
|
||||||
locationIds: string[],
|
locationIds: string[],
|
||||||
quantity: number
|
quantity: number
|
||||||
): Promise<boolean>
|
): Promise<boolean>
|
||||||
|
|
||||||
retrieveAvailableQuantity(
|
retrieveAvailableQuantity(
|
||||||
itemId: string,
|
inventoryItemId: string,
|
||||||
locationIds: string[]
|
locationIds: string[]
|
||||||
): Promise<number>
|
): Promise<number>
|
||||||
|
|
||||||
retrieveStockedQuantity(
|
retrieveStockedQuantity(
|
||||||
itemId: string,
|
inventoryItemId: string,
|
||||||
|
locationIds: string[]
|
||||||
|
): Promise<number>
|
||||||
|
|
||||||
|
retrieveReservedQuantity(
|
||||||
|
inventoryItemId: string,
|
||||||
locationIds: string[]
|
locationIds: string[]
|
||||||
): Promise<number>
|
): Promise<number>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() })
|
||||||
@@ -34,6 +34,7 @@ export type InventoryLevelDTO = {
|
|||||||
inventory_item_id: string
|
inventory_item_id: string
|
||||||
location_id: string
|
location_id: string
|
||||||
stocked_quantity: number
|
stocked_quantity: number
|
||||||
|
reserved_quantity: number
|
||||||
incoming_quantity: number
|
incoming_quantity: number
|
||||||
metadata: Record<string, unknown> | null
|
metadata: Record<string, unknown> | null
|
||||||
created_at: string | Date
|
created_at: string | Date
|
||||||
@@ -87,6 +88,7 @@ export type FilterableInventoryLevelProps = {
|
|||||||
inventory_item_id?: string | string[]
|
inventory_item_id?: string | string[]
|
||||||
location_id?: string | string[]
|
location_id?: string | string[]
|
||||||
stocked_quantity?: number | NumericalComparisonOperator
|
stocked_quantity?: number | NumericalComparisonOperator
|
||||||
|
reserved_quantity?: number | NumericalComparisonOperator
|
||||||
incoming_quantity?: number | NumericalComparisonOperator
|
incoming_quantity?: number | NumericalComparisonOperator
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +96,8 @@ export type CreateInventoryLevelInput = {
|
|||||||
inventory_item_id: string
|
inventory_item_id: string
|
||||||
location_id: string
|
location_id: string
|
||||||
stocked_quantity: number
|
stocked_quantity: number
|
||||||
incoming_quantity: number
|
reserved_quantity?: number
|
||||||
|
incoming_quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateInventoryLevelInput = {
|
export type UpdateInventoryLevelInput = {
|
||||||
|
|||||||
@@ -4332,6 +4332,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@medusajs/medusa-cli@^1.3.5, @medusajs/medusa-cli@workspace:packages/medusa-cli":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/medusa-cli@workspace:packages/medusa-cli"
|
resolution: "@medusajs/medusa-cli@workspace:packages/medusa-cli"
|
||||||
|
|||||||
Reference in New Issue
Block a user