feat(inventory): check stock to create reservation (#7209)

This commit is contained in:
Carlos R. L. Rodrigues
2024-05-05 05:24:52 -03:00
committed by GitHub
parent e97b0125b5
commit c32a31e139
6 changed files with 137 additions and 27 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/inventory-next": patch
"@medusajs/types": patch
---
Check stock to create reservation and support backorder

View File

@@ -45,6 +45,10 @@ export interface CreateReservationItemInput {
* The reserved quantity.
*/
quantity: number
/**
* Allow backorder of the item. If true, it won't check inventory levels before reserving it.
*/
allow_backorder?: boolean
/**
* The description of the reservation.
*/

View File

@@ -89,6 +89,41 @@ moduleIntegrationTestRunner({
expect(inventoryLevel.reserved_quantity).toEqual(2)
})
it("should check inventory levels before creating reservation", async () => {
const reserveMoreThanInStock = service.createReservationItems({
inventory_item_id: inventoryItem.id,
location_id: "location-1",
quantity: 3,
})
expect(reserveMoreThanInStock).rejects.toThrow(
`Not enough stock available for item ${inventoryItem.id} at location location-1`
)
let inventoryLevel =
await service.retrieveInventoryLevelByItemAndLocation(
inventoryItem.id,
"location-1"
)
expect(inventoryLevel.reserved_quantity).toEqual(0)
await service.createReservationItems({
inventory_item_id: inventoryItem.id,
location_id: "location-1",
allow_backorder: true,
quantity: 3,
})
inventoryLevel =
await service.retrieveInventoryLevelByItemAndLocation(
inventoryItem.id,
"location-1"
)
expect(inventoryLevel.reserved_quantity).toEqual(3)
})
it("should create reservationItems from array", async () => {
const data = [
{
@@ -99,6 +134,7 @@ moduleIntegrationTestRunner({
{
inventory_item_id: inventoryItem.id,
location_id: "location-2",
allow_backorder: true,
quantity: 3,
},
]

View File

@@ -1,39 +1,76 @@
import { Migration } from '@mikro-orm/migrations';
import { Migration } from "@mikro-orm/migrations"
export class Migration20240307132720 extends Migration {
async up(): Promise<void> {
this.addSql('create table if not exists "inventory_item" ("id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "sku" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "weight" int null, "length" int null, "height" int null, "width" int null, "requires_shipping" boolean not null default true, "description" text null, "title" text null, "thumbnail" text null, "metadata" jsonb null, constraint "inventory_item_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_inventory_item_deleted_at" ON "inventory_item" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_inventory_item_sku_unique" ON "inventory_item" (sku);');
this.addSql(
'create table if not exists "inventory_item" ("id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "sku" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "weight" int null, "length" int null, "height" int null, "width" int null, "requires_shipping" boolean not null default true, "description" text null, "title" text null, "thumbnail" text null, "metadata" jsonb null, constraint "inventory_item_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_inventory_item_deleted_at" ON "inventory_item" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_inventory_item_sku_unique" ON "inventory_item" (sku);'
)
this.addSql('create table if not exists "inventory_level" ("id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "inventory_item_id" text not null, "location_id" text not null, "stocked_quantity" int not null default 0, "reserved_quantity" int not null default 0, "incoming_quantity" int not null default 0, "metadata" jsonb null, constraint "inventory_level_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_inventory_level_deleted_at" ON "inventory_level" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_inventory_level_inventory_item_id" ON "inventory_level" (inventory_item_id);');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_inventory_level_location_id" ON "inventory_level" (location_id);');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_inventory_level_location_id" ON "inventory_level" (location_id);');
this.addSql(
'create table if not exists "inventory_level" ("id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "inventory_item_id" text not null, "location_id" text not null, "stocked_quantity" int not null default 0, "reserved_quantity" int not null default 0, "incoming_quantity" int not null default 0, "metadata" jsonb null, constraint "inventory_level_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_inventory_level_deleted_at" ON "inventory_level" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_inventory_level_inventory_item_id" ON "inventory_level" (inventory_item_id);'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_inventory_level_location_id" ON "inventory_level" (location_id);'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_inventory_level_location_id" ON "inventory_level" (location_id);'
)
this.addSql('create table if not exists "reservation_item" ("id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "line_item_id" text null, "location_id" text not null, "quantity" integer not null, "external_id" text null, "description" text null, "created_by" text null, "metadata" jsonb null, "inventory_item_id" text not null, constraint "reservation_item_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_reservation_item_deleted_at" ON "reservation_item" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_reservation_item_line_item_id" ON "reservation_item" (line_item_id);');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_reservation_item_location_id" ON "reservation_item" (location_id);');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_reservation_item_inventory_item_id" ON "reservation_item" (inventory_item_id);');
this.addSql(
'create table if not exists "reservation_item" ("id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "line_item_id" text null, "location_id" text not null, "quantity" integer not null, "external_id" text null, "description" text null, "created_by" text null, "metadata" jsonb null, "inventory_item_id" text not null, constraint "reservation_item_pkey" primary key ("id"));'
)
this.addSql('alter table if exists "inventory_level" add constraint "inventory_level_inventory_item_id_foreign" foreign key ("inventory_item_id") references "inventory_item" ("id") on update cascade on delete cascade;');
this.addSql(
`ALTER TABLE "reservation_item" ADD COLUMN IF NOT EXISTS "allow_backorder" boolean DEFAULT false;`
)
this.addSql('alter table if exists "reservation_item" add constraint "reservation_item_inventory_item_id_foreign" foreign key ("inventory_item_id") references "inventory_item" ("id") on update cascade on delete cascade;');
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_reservation_item_deleted_at" ON "reservation_item" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_reservation_item_line_item_id" ON "reservation_item" (line_item_id);'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_reservation_item_location_id" ON "reservation_item" (location_id);'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_reservation_item_inventory_item_id" ON "reservation_item" (inventory_item_id);'
)
this.addSql(
'alter table if exists "inventory_level" add constraint "inventory_level_inventory_item_id_foreign" foreign key ("inventory_item_id") references "inventory_item" ("id") on update cascade on delete cascade;'
)
this.addSql(
'alter table if exists "reservation_item" add constraint "reservation_item_inventory_item_id_foreign" foreign key ("inventory_item_id") references "inventory_item" ("id") on update cascade on delete cascade;'
)
}
async down(): Promise<void> {
this.addSql('alter table if exists "inventory_level" drop constraint if exists "inventory_level_inventory_item_id_foreign";');
this.addSql(
'alter table if exists "inventory_level" drop constraint if exists "inventory_level_inventory_item_id_foreign";'
)
this.addSql('alter table if exists "reservation_item" drop constraint if exists "reservation_item_inventory_item_id_foreign";');
this.addSql(
'alter table if exists "reservation_item" drop constraint if exists "reservation_item_inventory_item_id_foreign";'
)
this.addSql('drop table if exists "inventory_item" cascade;');
this.addSql('drop table if exists "inventory_item" cascade;')
this.addSql('drop table if exists "inventory_level" cascade;');
this.addSql('drop table if exists "inventory_level" cascade;')
this.addSql('drop table if exists "reservation_item" cascade;');
this.addSql('drop table if exists "reservation_item" cascade;')
}
}

View File

@@ -8,10 +8,12 @@ import {
Property,
} from "@mikro-orm/core"
import { DALUtils } from "@medusajs/utils"
import {
DALUtils,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/utils"
import { InventoryItem } from "./inventory-item"
import { createPsqlIndexStatementHelper } from "@medusajs/utils"
import { generateEntityId } from "@medusajs/utils"
const ReservationItemDeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "reservation_item",
@@ -62,6 +64,9 @@ export class ReservationItem {
@Property({ type: "text", nullable: true })
line_item_id: string | null = null
@Property({ type: "boolean" })
allow_backorder: boolean = false
@ReservationItemLocationIdIndex.MikroORMIndex()
@Property({ type: "text" })
location_id: string

View File

@@ -15,11 +15,12 @@ import {
InjectManager,
InjectTransactionManager,
InventoryEvents,
isDefined,
MedusaContext,
MedusaError,
ModulesSdkUtils,
isDefined,
partitionArray,
promiseAll,
} from "@medusajs/utils"
import { InventoryItem, InventoryLevel, ReservationItem } from "@models"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
@@ -170,6 +171,27 @@ export default class InventoryModuleService<
> {
const toCreate = Array.isArray(input) ? input : [input]
const checkLevels = toCreate.map(async (item) => {
if (!!item.allow_backorder) {
return
}
const available = await this.retrieveAvailableQuantity(
item.inventory_item_id,
[item.location_id],
context
)
if (available < item.quantity) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Not enough stock available for item ${item.inventory_item_id} at location ${item.location_id}`
)
}
})
await promiseAll(checkLevels)
const created = await this.createReservationItems_(toCreate, context)
context.messageAggregator?.saveRawMessageData(