chore(inventory, core-flows): big number support (#8204)

This commit is contained in:
Carlos R. L. Rodrigues authored and GitHub committed 2024-07-22 06:32:25 -03:00
1 parent c307972a99
commit fb29b958fa
27 files changed
+360 -215

No files matched your search

@@ -292,16 +292,6 @@ medusaIntegrationTestRunner({
}) })
it("should fail to update the location level to negative quantity", async () => { it("should fail to update the location level to negative quantity", async () => {
await api.post(
`/admin/inventory-items/${inventoryItem1.id}/location-levels`,
{
location_id: stockLocation1.id,
stocked_quantity: 17,
incoming_quantity: 2,
},
adminHeaders
)
const res = await api const res = await api
.post( .post(
`/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`, `/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`,
@@ -751,12 +741,6 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
await api.post(
`/admin/inventory-items/${inventoryItem1.id}/location-levels`,
{ location_id: stockLocation1.id },
adminHeaders
)
await api.post( await api.post(
`/admin/reservations`, `/admin/reservations`,
{ {
@@ -784,7 +768,7 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
).data ).data
expect(levelsResponse.count).toEqual(2) expect(levelsResponse.count).toEqual(1)
const res = await api.delete( const res = await api.delete(
`/admin/inventory-items/${inventoryItem1.id}`, `/admin/inventory-items/${inventoryItem1.id}`,
@@ -15,6 +15,7 @@ import {
StockLocationDTO, StockLocationDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { import {
BigNumber,
ContainerRegistrationKeys, ContainerRegistrationKeys,
ModuleRegistrationName, ModuleRegistrationName,
Modules, Modules,
@@ -345,6 +346,10 @@ medusaIntegrationTestRunner({
}) })
it("should create a order fulfillment and cancel it", async () => { it("should create a order fulfillment and cancel it", async () => {
const inventoryModule = container.resolve(
ModuleRegistrationName.INVENTORY
)
const order = await createOrderFixture({ container, product, location }) const order = await createOrderFixture({ container, product, location })
// Create a fulfillment // Create a fulfillment
@@ -389,9 +394,6 @@ medusaIntegrationTestRunner({
expect(orderFulfill.fulfillments).toHaveLength(1) expect(orderFulfill.fulfillments).toHaveLength(1)
expect(orderFulfill.items[0].detail.fulfilled_quantity).toEqual(1) expect(orderFulfill.items[0].detail.fulfilled_quantity).toEqual(1)
const inventoryModule = container.resolve(
ModuleRegistrationName.INVENTORY
)
const reservation = await inventoryModule.listReservationItems({ const reservation = await inventoryModule.listReservationItems({
line_item_id: order.items![0].id, line_item_id: order.items![0].id,
}) })
@@ -401,7 +403,7 @@ medusaIntegrationTestRunner({
inventoryItem.id, inventoryItem.id,
[location.id] [location.id]
) )
expect(stockAvailability).toEqual(1) expect(stockAvailability).toEqual(new BigNumber(1))
// Cancel the fulfillment // Cancel the fulfillment
const cancelFulfillmentData: OrderWorkflow.CancelOrderFulfillmentWorkflowInput = const cancelFulfillmentData: OrderWorkflow.CancelOrderFulfillmentWorkflowInput =
@@ -443,7 +445,7 @@ medusaIntegrationTestRunner({
await inventoryModule.retrieveStockedQuantity(inventoryItem.id, [ await inventoryModule.retrieveStockedQuantity(inventoryItem.id, [
location.id, location.id,
]) ])
expect(stockAvailabilityAfterCancelled).toEqual(2) expect(stockAvailabilityAfterCancelled.valueOf()).toEqual(2)
}) })
it("should revert an order fulfillment when it fails and recreate it when tried again", async () => { it("should revert an order fulfillment when it fails and recreate it when tried again", async () => {
@@ -518,7 +520,7 @@ medusaIntegrationTestRunner({
inventoryItem.id, inventoryItem.id,
[location.id] [location.id]
) )
expect(stockAvailability).toEqual(1) expect(stockAvailability.valueOf()).toEqual(1)
}) })
}) })
}, },
@@ -418,7 +418,7 @@ medusaIntegrationTestRunner({
inventoryItem.id, inventoryItem.id,
[location.id] [location.id]
) )
expect(stockAvailability).toEqual(1) expect(stockAvailability.valueOf()).toEqual(1)
}) })
}) })
}, },
@@ -1,7 +1,7 @@
import { IInventoryService, InventoryTypes } from "@medusajs/types" import { IInventoryService, InventoryTypes } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk" import { StepResponse, createStep } from "@medusajs/workflows-sdk"
import { ModuleRegistrationName } from "@medusajs/utils" import { MathBN, ModuleRegistrationName } from "@medusajs/utils"
export const adjustInventoryLevelsStepId = "adjust-inventory-levels-step" export const adjustInventoryLevelsStepId = "adjust-inventory-levels-step"
export const adjustInventoryLevelsStep = createStep( export const adjustInventoryLevelsStep = createStep(
@@ -30,7 +30,7 @@ export const adjustInventoryLevelsStep = createStep(
input.map((item) => { input.map((item) => {
return { return {
...item, ...item,
adjustment: item.adjustment * -1, adjustment: MathBN.mult(item.adjustment, -1),
} }
}) })
) )
@@ -1,11 +1,16 @@
import { FulfillmentDTO, OrderDTO, OrderWorkflow } from "@medusajs/types" import {
BigNumberInput,
FulfillmentDTO,
OrderDTO,
OrderWorkflow,
} from "@medusajs/types"
import { MedusaError, Modules } from "@medusajs/utils" import { MedusaError, Modules } from "@medusajs/utils"
import { import {
WorkflowData,
createStep, createStep,
createWorkflow, createWorkflow,
parallelize, parallelize,
transform, transform,
WorkflowData,
} from "@medusajs/workflows-sdk" } from "@medusajs/workflows-sdk"
import { useRemoteQueryStep } from "../../common" import { useRemoteQueryStep } from "../../common"
import { cancelFulfillmentWorkflow } from "../../fulfillment" import { cancelFulfillmentWorkflow } from "../../fulfillment"
@@ -82,7 +87,7 @@ function prepareInventoryUpdate({
const inventoryAdjustment: { const inventoryAdjustment: {
inventory_item_id: string inventory_item_id: string
location_id: string location_id: string
adjustment: number // TODO: BigNumberInput adjustment: BigNumberInput
}[] = [] }[] = []
for (const item of fulfillment.items) { for (const item of fulfillment.items) {
@@ -1,11 +1,12 @@
import { import {
BigNumberInput,
FulfillmentDTO, FulfillmentDTO,
FulfillmentWorkflow, FulfillmentWorkflow,
OrderDTO, OrderDTO,
OrderWorkflow, OrderWorkflow,
ReservationItemDTO, ReservationItemDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { MedusaError, Modules } from "@medusajs/utils" import { MathBN, MedusaError, Modules } from "@medusajs/utils"
import { import {
WorkflowData, WorkflowData,
createStep, createStep,
@@ -68,6 +69,7 @@ function prepareFulfillmentData({
order, order,
input, input,
shippingOption, shippingOption,
shippingMethod,
reservations, reservations,
}: { }: {
order: OrderDTO order: OrderDTO
@@ -77,6 +79,7 @@ function prepareFulfillmentData({
provider_id: string provider_id: string
service_zone: { fulfillment_set: { location?: { id: string } } } service_zone: { fulfillment_set: { location?: { id: string } } }
} }
shippingMethod: { data?: Record<string, unknown> | null }
reservations: ReservationItemDTO[] reservations: ReservationItemDTO[]
}) { }) {
const inputItems = input.items const inputItems = input.items
@@ -120,8 +123,9 @@ function prepareFulfillmentData({
location_id: locationId, location_id: locationId,
provider_id: shippingOption.provider_id, provider_id: shippingOption.provider_id,
shipping_option_id: shippingOption.id, shipping_option_id: shippingOption.id,
data: shippingMethod.data,
items: fulfillmentItems, items: fulfillmentItems,
labels: [] as FulfillmentWorkflow.CreateFulfillmentLabelWorkflowDTO[], // TODO: shipping labels labels: input.labels ?? [],
delivery_address: shippingAddress as any, delivery_address: shippingAddress as any,
}, },
} }
@@ -136,13 +140,13 @@ function prepareInventoryUpdate({ reservations, order, input, inputItemsMap }) {
const toDelete: string[] = [] const toDelete: string[] = []
const toUpdate: { const toUpdate: {
id: string id: string
quantity: number // TODO: BigNumberInput quantity: BigNumberInput
location_id: string location_id: string
}[] = [] }[] = []
const inventoryAdjustment: { const inventoryAdjustment: {
inventory_item_id: string inventory_item_id: string
location_id: string location_id: string
adjustment: number // TODO: BigNumberInput adjustment: BigNumberInput
}[] = [] }[] = []
for (const item of order.items) { for (const item of order.items) {
@@ -163,7 +167,7 @@ function prepareInventoryUpdate({ reservations, order, input, inputItemsMap }) {
inventoryAdjustment.push({ inventoryAdjustment.push({
inventory_item_id: reservation.inventory_item_id, inventory_item_id: reservation.inventory_item_id,
location_id: input.location_id ?? reservation.location_id, location_id: input.location_id ?? reservation.location_id,
adjustment: -item.quantity, // TODO: MathBN.mul(-1, item.quantity) adjustment: MathBN.mult(item.quantity, -1),
}) })
if (quantity === 0) { if (quantity === 0) {
@@ -201,6 +205,7 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
"items.variant.manage_inventory", "items.variant.manage_inventory",
"shipping_address.*", "shipping_address.*",
"shipping_methods.shipping_option_id", // TODO: which shipping method to use when multiple? "shipping_methods.shipping_option_id", // TODO: which shipping method to use when multiple?
"shipping_methods.data",
], ],
variables: { id: input.order_id }, variables: { id: input.order_id },
list: false, list: false,
@@ -216,6 +221,10 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
}, {}) }, {})
}) })
const shippingMethod = transform(order, (data) => {
return { data: data.shipping_methods?.[0].data }
})
const shippingOptionId = transform(order, (data) => { const shippingOptionId = transform(order, (data) => {
return data.shipping_methods?.[0]?.shipping_option_id return data.shipping_methods?.[0]?.shipping_option_id
}) })
@@ -250,7 +259,7 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
}).config({ name: "get-reservations" }) }).config({ name: "get-reservations" })
const fulfillmentData = transform( const fulfillmentData = transform(
{ order, input, shippingOption, reservations }, { order, input, shippingOption, shippingMethod, reservations },
prepareFulfillmentData prepareFulfillmentData
) )
@@ -278,12 +287,12 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
prepareInventoryUpdate prepareInventoryUpdate
) )
adjustInventoryLevelsStep(inventoryAdjustment)
parallelize( parallelize(
registerOrderFulfillmentStep(registerOrderFulfillmentData), registerOrderFulfillmentStep(registerOrderFulfillmentData),
createRemoteLinkStep(link), createRemoteLinkStep(link),
updateReservationsStep(toUpdate), updateReservationsStep(toUpdate),
deleteReservationsStep(toDelete), deleteReservationsStep(toDelete)
adjustInventoryLevelsStep(inventoryAdjustment)
) )
// trigger event OrderModuleService.Events.FULFILLMENT_CREATED // trigger event OrderModuleService.Events.FULFILLMENT_CREATED
@@ -92,7 +92,7 @@ export const createOrderShipmentWorkflow = createWorkflow(
const fulfillmentData = transform({ input }, ({ input }) => { const fulfillmentData = transform({ input }, ({ input }) => {
return { return {
id: input.fulfillment_id, id: input.fulfillment_id,
labels: input.labels, labels: input.labels ?? [],
} }
}) })
@@ -161,7 +161,7 @@ function prepareFulfillmentData({
provider_id: returnShippingOption.provider_id, provider_id: returnShippingOption.provider_id,
shipping_option_id: input.return_shipping?.option_id, shipping_option_id: input.return_shipping?.option_id,
items: fulfillmentItems, items: fulfillmentItems,
labels: [] as FulfillmentWorkflow.CreateFulfillmentLabelWorkflowDTO[], labels: input.return_shipping?.labels ?? [],
delivery_address: order.shipping_address ?? ({} as any), // TODO: should it be the stock location address? delivery_address: order.shipping_address ?? ({} as any), // TODO: should it be the stock location address?
order: order, order: order,
}, },
@@ -2,6 +2,7 @@ import {
NumericalComparisonOperator, NumericalComparisonOperator,
StringComparisonOperator, StringComparisonOperator,
} from "../../common" } from "../../common"
import { BigNumberInput } from "../../totals/big-number"
/** /**
* The reservation item details. * The reservation item details.
@@ -25,7 +26,7 @@ export interface ReservationItemDTO {
/** /**
* The quantity of the reservation item. * The quantity of the reservation item.
*/ */
quantity: number quantity: BigNumberInput
/** /**
* The associated line item's ID. * The associated line item's ID.
@@ -1,3 +1,5 @@
import { BigNumberInput } from "../../totals/big-number"
export interface CreateInventoryLevelInput { export interface CreateInventoryLevelInput {
/** /**
* The ID of the associated inventory item. * The ID of the associated inventory item.
@@ -66,5 +68,5 @@ export type BulkAdjustInventoryLevelInput = {
/** /**
* The quantity to adjust the inventory level by. * The quantity to adjust the inventory level by.
*/ */
adjustment: number // TODO: BigNumberInput adjustment: BigNumberInput
} & UpdateInventoryLevelInput } & UpdateInventoryLevelInput
@@ -1,3 +1,5 @@
import { BigNumberInput } from "../../totals"
/** /**
* @interface * @interface
* *
@@ -8,7 +10,7 @@ export interface UpdateReservationItemInput {
/** /**
* The reserved quantity. * The reserved quantity.
*/ */
quantity?: number quantity?: BigNumberInput
/** /**
* The ID of the associated location. * The ID of the associated location.
*/ */
@@ -48,7 +50,7 @@ export interface CreateReservationItemInput {
/** /**
* The reserved quantity. * The reserved quantity.
*/ */
quantity: number quantity: BigNumberInput
/** /**
* Allow backorder of the item. If true, it won't check inventory levels before reserving it. * Allow backorder of the item. If true, it won't check inventory levels before reserving it.
*/ */
+10 -9
View File
@@ -3,6 +3,7 @@ import { RestoreReturn, SoftDeleteReturn } from "../dal"
import { FindConfig } from "../common" import { FindConfig } from "../common"
import { IModuleService } from "../modules-sdk" import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context" import { Context } from "../shared-context"
import { BigNumberInput, IBigNumber } from "../totals"
import { import {
FilterableInventoryItemProps, FilterableInventoryItemProps,
FilterableInventoryLevelProps, FilterableInventoryLevelProps,
@@ -1044,7 +1045,7 @@ export interface IInventoryService extends IModuleService {
data: { data: {
inventoryItemId: string inventoryItemId: string
locationId: string locationId: string
adjustment: number adjustment: BigNumberInput
}[], }[],
context?: Context context?: Context
): Promise<InventoryLevelDTO[]> ): Promise<InventoryLevelDTO[]>
@@ -1052,7 +1053,7 @@ export interface IInventoryService extends IModuleService {
adjustInventory( adjustInventory(
inventoryItemId: string, inventoryItemId: string,
locationId: string, locationId: string,
adjustment: number, adjustment: BigNumberInput,
context?: Context context?: Context
): Promise<InventoryLevelDTO> ): Promise<InventoryLevelDTO>
@@ -1076,7 +1077,7 @@ export interface IInventoryService extends IModuleService {
confirmInventory( confirmInventory(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
quantity: number, quantity: BigNumberInput,
context?: Context context?: Context
): Promise<boolean> ): Promise<boolean>
@@ -1086,7 +1087,7 @@ export interface IInventoryService extends IModuleService {
* @param {string} inventoryItemId - The inventory item's ID. * @param {string} inventoryItemId - The inventory item's ID.
* @param {string[]} locationIds - The locations' IDs. * @param {string[]} locationIds - The locations' IDs.
* @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<number>} The available quantity of the item. * @returns {Promise<BigNumber>} The available quantity of the item.
* *
* @example * @example
* const availableQuantity = * const availableQuantity =
@@ -1099,7 +1100,7 @@ export interface IInventoryService extends IModuleService {
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
context?: Context context?: Context
): Promise<number> ): Promise<IBigNumber>
/** /**
* This method retrieves the stocked quantity of an inventory item in the specified location. * This method retrieves the stocked quantity of an inventory item in the specified location.
@@ -1107,7 +1108,7 @@ export interface IInventoryService extends IModuleService {
* @param {string} inventoryItemId - The inventory item's ID. * @param {string} inventoryItemId - The inventory item's ID.
* @param {string[]} locationIds - The locations' IDs. * @param {string[]} locationIds - The locations' IDs.
* @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<number>} The stocked quantity of the item. * @returns {Promise<BigNumber>} The stocked quantity of the item.
* *
* @example * @example
* const stockedQuantity = * const stockedQuantity =
@@ -1120,7 +1121,7 @@ export interface IInventoryService extends IModuleService {
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
context?: Context context?: Context
): Promise<number> ): Promise<IBigNumber>
/** /**
* This method retrieves the reserved quantity of an inventory item in the specified location. * This method retrieves the reserved quantity of an inventory item in the specified location.
@@ -1128,7 +1129,7 @@ export interface IInventoryService extends IModuleService {
* @param {string} inventoryItemId - The inventory item's ID. * @param {string} inventoryItemId - The inventory item's ID.
* @param {string[]} locationIds - The locations' IDs. * @param {string[]} locationIds - The locations' IDs.
* @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<number>} The reserved quantity of the item. * @returns {Promise<BigNumber>} The reserved quantity of the item.
* *
* @example * @example
* const reservedQuantity = * const reservedQuantity =
@@ -1141,5 +1142,5 @@ export interface IInventoryService extends IModuleService {
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
context?: Context context?: Context
): Promise<number> ): Promise<IBigNumber>
} }
+16 -2
View File
@@ -1,10 +1,24 @@
import BigNumberJS from "bignumber.js" import BigNumberJS from "bignumber.js"
export interface IBigNumber {
numeric: number
raw?: BigNumberRawValue
bigNumber?: BigNumberJS
toJSON(): number
valueOf(): number
}
export type BigNumberRawValue = { export type BigNumberRawValue = {
value: string | number value: string | number
[key: string]: unknown [key: string]: unknown
} }
export type BigNumberInput = BigNumberRawValue | number | string | BigNumberJS export type BigNumberInput =
| BigNumberRawValue
| number
| string
| BigNumberJS
| IBigNumber
export type BigNumberValue = BigNumberJS | number | string export type BigNumberValue = BigNumberJS | number | string | IBigNumber
@@ -1,4 +1,5 @@
import { BigNumberInput } from "../../totals" import { BigNumberInput } from "../../totals"
import { CreateFulfillmentLabelWorkflowDTO } from "../fulfillment/create-fulfillment"
interface CreateOrderFulfillmentItem { interface CreateOrderFulfillmentItem {
id: string id: string
@@ -9,6 +10,7 @@ export interface CreateOrderFulfillmentWorkflowInput {
order_id: string order_id: string
created_by?: string // The id of the authenticated user created_by?: string // The id of the authenticated user
items: CreateOrderFulfillmentItem[] items: CreateOrderFulfillmentItem[]
labels?: CreateFulfillmentLabelWorkflowDTO[]
no_notification?: boolean no_notification?: boolean
location_id?: string | null location_id?: string | null
metadata?: Record<string, any> | null metadata?: Record<string, any> | null
@@ -1,4 +1,5 @@
import { BigNumberInput } from "../../totals" import { BigNumberInput } from "../../totals"
import { CreateFulfillmentLabelWorkflowDTO } from "../fulfillment/create-fulfillment"
export interface CreateReturnItem { export interface CreateReturnItem {
id: string id: string
@@ -16,6 +17,7 @@ export interface CreateOrderReturnWorkflowInput {
return_shipping?: { return_shipping?: {
option_id: string option_id: string
price?: number price?: number
labels?: CreateFulfillmentLabelWorkflowDTO[]
} }
note?: string | null note?: string | null
receive_now?: boolean receive_now?: boolean
@@ -12,7 +12,7 @@ export interface CreateOrderShipmentWorkflowInput {
fulfillment_id: string fulfillment_id: string
created_by?: string // The id of the authenticated user created_by?: string // The id of the authenticated user
items: CreateOrderShipmentItem[] items: CreateOrderShipmentItem[]
labels: CreateFulfillmentLabelWorkflowDTO[] labels?: CreateFulfillmentLabelWorkflowDTO[]
no_notification?: boolean no_notification?: boolean
metadata?: MetadataType metadata?: MetadataType
} }
+4 -4
View File
@@ -1,8 +1,8 @@
import { BigNumberInput, BigNumberRawValue } from "@medusajs/types" import { BigNumberInput, BigNumberRawValue, IBigNumber } from "@medusajs/types"
import { BigNumber as BigNumberJS } from "bignumber.js" import { BigNumber as BigNumberJS } from "bignumber.js"
import { isBigNumber, isString } from "../common" import { isBigNumber, isString } from "../common"
export class BigNumber { export class BigNumber implements IBigNumber {
static DEFAULT_PRECISION = 20 static DEFAULT_PRECISION = 20
private numeric_: number private numeric_: number
@@ -110,7 +110,7 @@ export class BigNumber {
this.bignumber_ = newValue.bignumber_ this.bignumber_ = newValue.bignumber_
} }
toJSON() { toJSON(): number {
return this.bignumber_ return this.bignumber_
? this.bignumber_?.toNumber() ? this.bignumber_?.toNumber()
: this.raw_ : this.raw_
@@ -118,7 +118,7 @@ export class BigNumber {
: this.numeric_ : this.numeric_
} }
valueOf() { valueOf(): number {
return this.numeric_ return this.numeric_
} }
} }
@@ -1,6 +1,6 @@
import { IInventoryService, InventoryItemDTO } from "@medusajs/types" import { IInventoryService, InventoryItemDTO } from "@medusajs/types"
import { BigNumber, Module, Modules } from "@medusajs/utils"
import { moduleIntegrationTestRunner } from "medusa-test-utils" import { moduleIntegrationTestRunner } from "medusa-test-utils"
import { Module, Modules } from "@medusajs/utils"
import { InventoryModuleService } from "../../src/services" import { InventoryModuleService } from "../../src/services"
jest.setTimeout(100000) jest.setTimeout(100000)
@@ -882,7 +882,7 @@ moduleIntegrationTestRunner<IInventoryService>({
["location-1", "location-2"] ["location-1", "location-2"]
) )
expect(level).toEqual(6) expect(level).toEqual(new BigNumber(6))
}) })
}) })
@@ -930,7 +930,7 @@ moduleIntegrationTestRunner<IInventoryService>({
["location-1", "location-2"] ["location-1", "location-2"]
) )
expect(stockedQuantity).toEqual(8) expect(stockedQuantity.valueOf()).toEqual(8)
}) })
}) })
@@ -991,12 +991,12 @@ moduleIntegrationTestRunner<IInventoryService>({
}) })
it("retrieves reserved quantity", async () => { it("retrieves reserved quantity", async () => {
const reservedQuantity = await service.retrieveReservedQuantity( const reservedQuantity = (await service.retrieveReservedQuantity(
inventoryItem.id, inventoryItem.id,
["location-1", "location-2"] ["location-1", "location-2"]
) )) as any
expect(reservedQuantity).toEqual(2) expect(reservedQuantity + 0).toEqual(2)
}) })
}) })
@@ -24,9 +24,6 @@ export class Migration20240307132720 extends Migration {
this.addSql( this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_inventory_level_location_id" ON "inventory_level" (location_id);' '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( 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"));' '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"));'
@@ -0,0 +1,73 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20240719123015 extends Migration {
async up(): Promise<void> {
this.addSql(
`
ALTER TABLE "reservation_item" ALTER COLUMN "quantity" TYPE numeric;
ALTER TABLE "reservation_item" ADD COLUMN IF NOT EXISTS "raw_quantity" JSONB NULL;
ALTER TABLE "inventory_level" ALTER COLUMN "stocked_quantity" TYPE numeric;
ALTER TABLE "inventory_level" ADD COLUMN IF NOT EXISTS "raw_stocked_quantity" JSONB NULL;
ALTER TABLE "inventory_level" ALTER COLUMN "reserved_quantity" TYPE numeric;
ALTER TABLE "inventory_level" ADD COLUMN IF NOT EXISTS "raw_reserved_quantity" JSONB NULL;
ALTER TABLE "inventory_level" ALTER COLUMN "incoming_quantity" TYPE numeric;
ALTER TABLE "inventory_level" ADD COLUMN IF NOT EXISTS "raw_incoming_quantity" JSONB NULL;
DROP INDEX IF EXISTS "IDX_inventory_item_sku_unique";
DROP INDEX IF EXISTS "IDX_inventory_level_inventory_item_id";
DROP INDEX IF EXISTS "IDX_inventory_level_location_id";
DROP INDEX IF EXISTS "IDX_reservation_item_line_item_id";
DROP INDEX IF EXISTS "IDX_reservation_item_location_id";
DROP INDEX IF EXISTS "IDX_reservation_item_inventory_item_id";
CREATE UNIQUE INDEX IF NOT EXISTS "IDX_inventory_item_sku_unique" ON "inventory_item" (sku) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_inventory_level_inventory_item_id" ON "inventory_level" (inventory_item_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_inventory_level_location_id" ON "inventory_level" (location_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_reservation_item_line_item_id" ON "reservation_item" (line_item_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_reservation_item_location_id" ON "reservation_item" (location_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_reservation_item_inventory_item_id" ON "reservation_item" (inventory_item_id) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX "IDX_inventory_level_item_location" ON "inventory_level" (inventory_item_id, location_id) WHERE deleted_at IS NULL;
`
)
}
async down(): Promise<void> {
this.addSql(
`
ALTER TABLE "reservation_item" ALTER COLUMN "quantity" TYPE integer;
ALTER TABLE "reservation_item" DROP COLUMN IF EXISTS "raw_quantity";
ALTER TABLE "inventory_level" ALTER COLUMN "stocked_quantity" TYPE integer;
ALTER TABLE "inventory_level" DROP COLUMN IF NOT EXISTS "raw_stocked_quantity";
ALTER TABLE "inventory_level" ALTER COLUMN "reserved_quantity" TYPE integer;
ALTER TABLE "inventory_level" DROP COLUMN IF NOT EXISTS "raw_reserved_quantity";
ALTER TABLE "inventory_level" ALTER COLUMN "incoming_quantity" TYPE integer;
ALTER TABLE "inventory_level" DROP COLUMN IF NOT EXISTS "raw_incoming_quantity";
DROP INDEX IF EXISTS "IDX_inventory_item_sku_unique";
DROP INDEX IF EXISTS "IDX_inventory_level_inventory_item_id";
DROP INDEX IF EXISTS "IDX_inventory_level_location_id";
DROP INDEX IF EXISTS "IDX_reservation_item_line_item_id";
DROP INDEX IF EXISTS "IDX_reservation_item_location_id";
DROP INDEX IF EXISTS "IDX_reservation_item_inventory_item_id";
CREATE UNIQUE INDEX IF NOT EXISTS "IDX_inventory_item_sku_unique" ON "inventory_item" (sku);
CREATE INDEX IF NOT EXISTS "IDX_inventory_level_inventory_item_id" ON "inventory_level" (inventory_item_id);
CREATE INDEX IF NOT EXISTS "IDX_inventory_level_location_id" ON "inventory_level" (location_id);
CREATE INDEX IF NOT EXISTS "IDX_reservation_item_line_item_id" ON "reservation_item" (line_item_id);
CREATE INDEX IF NOT EXISTS "IDX_reservation_item_location_id" ON "reservation_item" (location_id);
CREATE INDEX IF NOT EXISTS "IDX_reservation_item_inventory_item_id" ON "reservation_item" (inventory_item_id);
DROP INDEX IF EXISTS "IDX_inventory_level_item_location"
`
)
}
}
@@ -32,6 +32,7 @@ const InventoryItemSkuIndex = createPsqlIndexStatementHelper({
tableName: "inventory_item", tableName: "inventory_item",
columns: "sku", columns: "sku",
unique: true, unique: true,
where: "deleted_at IS NULL",
}) })
type InventoryItemOptionalProps = DAL.SoftDeletableModelDateColumns type InventoryItemOptionalProps = DAL.SoftDeletableModelDateColumns
@@ -1,4 +1,4 @@
import { DALUtils, isDefined } from "@medusajs/utils" import { DALUtils, isDefined, MathBN } from "@medusajs/utils"
import { import {
BeforeCreate, BeforeCreate,
Entity, Entity,
@@ -11,9 +11,12 @@ import {
Rel, Rel,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { BigNumberRawValue } from "@medusajs/types"
import { import {
BigNumber,
createPsqlIndexStatementHelper, createPsqlIndexStatementHelper,
generateEntityId, generateEntityId,
MikroOrmBigNumberProperty,
} from "@medusajs/utils" } from "@medusajs/utils"
import { InventoryItem } from "./inventory-item" import { InventoryItem } from "./inventory-item"
@@ -26,17 +29,21 @@ const InventoryLevelDeletedAtIndex = createPsqlIndexStatementHelper({
const InventoryLevelInventoryItemIdIndex = createPsqlIndexStatementHelper({ const InventoryLevelInventoryItemIdIndex = createPsqlIndexStatementHelper({
tableName: "inventory_level", tableName: "inventory_level",
columns: "inventory_item_id", columns: "inventory_item_id",
where: "deleted_at IS NULL",
}) })
const InventoryLevelLocationIdIndex = createPsqlIndexStatementHelper({ const InventoryLevelLocationIdIndex = createPsqlIndexStatementHelper({
tableName: "inventory_level", tableName: "inventory_level",
columns: "location_id", columns: "location_id",
where: "deleted_at IS NULL",
}) })
const InventoryLevelLocationIdInventoryItemIdIndex = const InventoryLevelLocationIdInventoryItemIdIndex =
createPsqlIndexStatementHelper({ createPsqlIndexStatementHelper({
tableName: "inventory_level", tableName: "inventory_level",
columns: "location_id", columns: ["inventory_item_id", "location_id"],
unique: true,
where: "deleted_at IS NULL",
}) })
@Entity() @Entity()
@@ -78,14 +85,23 @@ export class InventoryLevel {
@Property({ type: "text" }) @Property({ type: "text" })
location_id: string location_id: string
@Property({ type: "int" }) @MikroOrmBigNumberProperty()
stocked_quantity: number = 0 stocked_quantity: BigNumber | number = 0
@Property({ type: "int" }) @Property({ columnType: "jsonb" })
reserved_quantity: number = 0 raw_stocked_quantity: BigNumberRawValue
@Property({ type: "int" }) @MikroOrmBigNumberProperty()
incoming_quantity: number = 0 reserved_quantity: BigNumber | number = 0
@Property({ columnType: "jsonb" })
raw_reserved_quantity: BigNumberRawValue
@MikroOrmBigNumberProperty()
incoming_quantity: BigNumber | number = 0
@Property({ columnType: "jsonb" })
raw_incoming_quantity: BigNumberRawValue
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null metadata: Record<string, unknown> | null
@@ -95,7 +111,7 @@ export class InventoryLevel {
}) })
inventory_item: Rel<InventoryItem> inventory_item: Rel<InventoryItem>
available_quantity: number | null = null available_quantity: BigNumber | number | null = null
@BeforeCreate() @BeforeCreate()
private beforeCreate(): void { private beforeCreate(): void {
@@ -111,7 +127,9 @@ export class InventoryLevel {
@OnLoad() @OnLoad()
private onLoad(): void { private onLoad(): void {
if (isDefined(this.stocked_quantity) && isDefined(this.reserved_quantity)) { if (isDefined(this.stocked_quantity) && isDefined(this.reserved_quantity)) {
this.available_quantity = this.stocked_quantity - this.reserved_quantity this.available_quantity = new BigNumber(
MathBN.sub(this.raw_stocked_quantity, this.raw_reserved_quantity)
)
} }
} }
} }
@@ -9,8 +9,11 @@ import {
Rel, Rel,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { BigNumberRawValue } from "@medusajs/types"
import { import {
BigNumber,
DALUtils, DALUtils,
MikroOrmBigNumberProperty,
createPsqlIndexStatementHelper, createPsqlIndexStatementHelper,
generateEntityId, generateEntityId,
} from "@medusajs/utils" } from "@medusajs/utils"
@@ -24,16 +27,19 @@ const ReservationItemDeletedAtIndex = createPsqlIndexStatementHelper({
const ReservationItemLineItemIdIndex = createPsqlIndexStatementHelper({ const ReservationItemLineItemIdIndex = createPsqlIndexStatementHelper({
tableName: "reservation_item", tableName: "reservation_item",
columns: "line_item_id", columns: "line_item_id",
where: "deleted_at IS NULL",
}) })
const ReservationItemInventoryItemIdIndex = createPsqlIndexStatementHelper({ const ReservationItemInventoryItemIdIndex = createPsqlIndexStatementHelper({
tableName: "reservation_item", tableName: "reservation_item",
columns: "inventory_item_id", columns: "inventory_item_id",
where: "deleted_at IS NULL",
}) })
const ReservationItemLocationIdIndex = createPsqlIndexStatementHelper({ const ReservationItemLocationIdIndex = createPsqlIndexStatementHelper({
tableName: "reservation_item", tableName: "reservation_item",
columns: "location_id", columns: "location_id",
where: "deleted_at IS NULL",
}) })
@Entity() @Entity()
@@ -72,8 +78,11 @@ export class ReservationItem {
@Property({ type: "text" }) @Property({ type: "text" })
location_id: string location_id: string
@Property({ columnType: "integer" }) @MikroOrmBigNumberProperty()
quantity: number quantity: BigNumber | number
@Property({ columnType: "jsonb" })
raw_quantity: BigNumberRawValue
@Property({ type: "text", nullable: true }) @Property({ type: "text", nullable: true })
external_id: string | null = null external_id: string | null = null
@@ -1,7 +1,11 @@
import { Context } from "@medusajs/types" import { Context } from "@medusajs/types"
import { InventoryLevel } from "@models" import {
BigNumber,
MathBN,
mikroOrmBaseRepositoryFactory,
} from "@medusajs/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { mikroOrmBaseRepositoryFactory } from "@medusajs/utils" import { InventoryLevel } from "@models"
export class InventoryLevelRepository extends mikroOrmBaseRepositoryFactory( export class InventoryLevelRepository extends mikroOrmBaseRepositoryFactory(
InventoryLevel InventoryLevel
@@ -10,42 +14,42 @@ export class InventoryLevelRepository extends mikroOrmBaseRepositoryFactory(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
context: Context = {} context: Context = {}
): Promise<number> { ): Promise<BigNumber> {
const manager = super.getActiveManager<SqlEntityManager>(context) const manager = super.getActiveManager<SqlEntityManager>(context)
const [result] = (await manager const result = await manager
.getKnex()({ il: "inventory_level" }) .getKnex()({ il: "inventory_level" })
.sum("reserved_quantity") .select("raw_reserved_quantity")
.whereIn("location_id", locationIds) .whereIn("location_id", locationIds)
.andWhere("inventory_item_id", inventoryItemId)) as { .andWhere("inventory_item_id", inventoryItemId)
sum: string .andWhereRaw("deleted_at IS NULL")
}[]
return parseInt(result.sum) return new BigNumber(
MathBN.sum(...result.map((r) => r.raw_reserved_quantity))
)
} }
async getAvailableQuantity( async getAvailableQuantity(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
context: Context = {} context: Context = {}
): Promise<number> { ): Promise<BigNumber> {
const knex = super.getActiveManager<SqlEntityManager>(context).getKnex() const knex = super.getActiveManager<SqlEntityManager>(context).getKnex()
const [result] = (await knex({ const result = await knex({
il: "inventory_level", il: "inventory_level",
}) })
.sum({ .select("raw_stocked_quantity", "raw_reserved_quantity")
stocked_quantity: "stocked_quantity",
reserved_quantity: "reserved_quantity",
})
.whereIn("location_id", locationIds) .whereIn("location_id", locationIds)
.andWhere("inventory_item_id", inventoryItemId)) as { .andWhere("inventory_item_id", inventoryItemId)
reserved_quantity: string .andWhereRaw("deleted_at IS NULL")
stocked_quantity: string
}[]
return ( return new BigNumber(
parseInt(result.stocked_quantity) - parseInt(result.reserved_quantity) MathBN.sum(
...result.map((r) => {
return MathBN.sub(r.raw_stocked_quantity, r.raw_reserved_quantity)
})
)
) )
} }
@@ -53,20 +57,19 @@ export class InventoryLevelRepository extends mikroOrmBaseRepositoryFactory(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
context: Context = {} context: Context = {}
): Promise<number> { ): Promise<BigNumber> {
const knex = super.getActiveManager<SqlEntityManager>(context).getKnex() const knex = super.getActiveManager<SqlEntityManager>(context).getKnex()
const [result] = (await knex({ const result = await knex({
il: "inventory_level", il: "inventory_level",
}) })
.sum({ .select("raw_stocked_quantity")
stocked_quantity: "stocked_quantity",
})
.whereIn("location_id", locationIds) .whereIn("location_id", locationIds)
.andWhere("inventory_item_id", inventoryItemId)) as { .andWhere("inventory_item_id", inventoryItemId)
stocked_quantity: string .andWhereRaw("deleted_at IS NULL")
}[]
return parseInt(result.stocked_quantity) return new BigNumber(
MathBN.sum(...result.map((r) => r.raw_stocked_quantity))
)
} }
} }
@@ -1,5 +1,5 @@
import { Context } from "@medusajs/types" import { Context } from "@medusajs/types"
import { ModulesSdkUtils } from "@medusajs/utils" import { BigNumber, ModulesSdkUtils } from "@medusajs/utils"
import { InventoryLevelRepository } from "@repositories" import { InventoryLevelRepository } from "@repositories"
import { InventoryLevel } from "../models/inventory-level" import { InventoryLevel } from "../models/inventory-level"
@@ -23,7 +23,7 @@ export default class InventoryLevelService extends ModulesSdkUtils.MedusaInterna
inventoryItemId: string, inventoryItemId: string,
locationIds: string[] | string, locationIds: string[] | string,
context: Context = {} context: Context = {}
): Promise<number> { ): Promise<BigNumber> {
const locationIdArray = Array.isArray(locationIds) const locationIdArray = Array.isArray(locationIds)
? locationIds ? locationIds
: [locationIds] : [locationIds]
@@ -39,7 +39,7 @@ export default class InventoryLevelService extends ModulesSdkUtils.MedusaInterna
inventoryItemId: string, inventoryItemId: string,
locationIds: string[] | string, locationIds: string[] | string,
context: Context = {} context: Context = {}
): Promise<number> { ): Promise<BigNumber> {
const locationIdArray = Array.isArray(locationIds) const locationIdArray = Array.isArray(locationIds)
? locationIds ? locationIds
: [locationIds] : [locationIds]
@@ -1,5 +1,6 @@
import { InternalModuleDeclaration } from "@medusajs/modules-sdk" import { InternalModuleDeclaration } from "@medusajs/modules-sdk"
import { import {
BigNumberInput,
Context, Context,
DAL, DAL,
InventoryTypes, InventoryTypes,
@@ -11,19 +12,20 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
import { IInventoryService } from "@medusajs/types/dist/inventory" import { IInventoryService } from "@medusajs/types/dist/inventory"
import { import {
arrayDifference, BigNumber,
CommonEvents, CommonEvents,
EmitEvents, EmitEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
InventoryEvents, InventoryEvents,
isDefined, MathBN,
isString,
MedusaContext, MedusaContext,
MedusaError, MedusaError,
MedusaService, MedusaService,
arrayDifference,
isDefined,
isString,
partitionArray, partitionArray,
promiseAll,
} from "@medusajs/utils" } from "@medusajs/utils"
import { InventoryItem, InventoryLevel, ReservationItem } from "@models" import { InventoryItem, InventoryLevel, ReservationItem } from "@models"
import { joinerConfig } from "../joiner-config" import { joinerConfig } from "../joiner-config"
@@ -36,6 +38,14 @@ type InjectedDependencies = {
reservationItemService: ModulesSdkTypes.IMedusaInternalService<any> reservationItemService: ModulesSdkTypes.IMedusaInternalService<any>
} }
type InventoryItemCheckLevel = {
id?: string
location_id: string
inventory_item_id: string
quantity?: BigNumberInput
allow_backorder?: boolean
}
export default class InventoryModuleService export default class InventoryModuleService
extends MedusaService<{ extends MedusaService<{
InventoryItem: { InventoryItem: {
@@ -84,14 +94,23 @@ export default class InventoryModuleService
} }
private async ensureInventoryLevels( private async ensureInventoryLevels(
data: ( data: InventoryItemCheckLevel[],
| { location_id: string; inventory_item_id: string } options?: {
| { id: string } validateQuantityAtLocation?: boolean
)[], },
context: Context context?: Context
): Promise<InventoryTypes.InventoryLevelDTO[]> { ): Promise<InventoryTypes.InventoryLevelDTO[]> {
options ??= {}
const validateQuantityAtLocation =
options.validateQuantityAtLocation ?? false
const data_ = data.map((dt: any) => ({
location_id: dt.location_id,
inventory_item_id: dt.inventory_item_id,
})) as InventoryItemCheckLevel[]
const [idData, itemLocationData] = partitionArray( const [idData, itemLocationData] = partitionArray(
data, data_,
({ id }) => !!id ({ id }) => !!id
) as [ ) as [
{ id: string }[], { id: string }[],
@@ -122,13 +141,14 @@ export default class InventoryModuleService
return acc return acc
}, new Map()) }, new Map())
const missing = data.filter((i) => { const missing = data.filter((item) => {
if ("id" in i) { if (item.id) {
return !inventoryLevelIdMap.has(i.id) return !inventoryLevelIdMap.has(item.id!)
} }
return !inventoryLevelItemLocationMap return !inventoryLevelItemLocationMap
.get(i.inventory_item_id) .get(item.inventory_item_id)
?.has(i.location_id) ?.has(item.location_id)
}) })
if (missing.length) { if (missing.length) {
@@ -144,6 +164,27 @@ export default class InventoryModuleService
throw new MedusaError(MedusaError.Types.NOT_FOUND, error) throw new MedusaError(MedusaError.Types.NOT_FOUND, error)
} }
if (validateQuantityAtLocation) {
for (const item of data) {
if (!!item.allow_backorder) {
continue
}
const locations = inventoryLevelItemLocationMap.get(
item.inventory_item_id
)!
const level = locations?.get(item.location_id)!
if (MathBN.lt(level.available_quantity, item.quantity!)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Not enough stock available for item ${item.inventory_item_id} at location ${item.location_id}`
)
}
}
}
return inventoryLevels return inventoryLevels
} }
@@ -151,7 +192,7 @@ export default class InventoryModuleService
// We sanitize the inputs here to prevent that from being used to update it // We sanitize the inputs here to prevent that from being used to update it
private sanitizeInventoryLevelInput<TDTO = unknown>( private sanitizeInventoryLevelInput<TDTO = unknown>(
input: (TDTO & { input: (TDTO & {
reserved_quantity?: number reserved_quantity?: BigNumberInput
})[] })[]
): TDTO[] { ): TDTO[] {
return input.map((input) => { return input.map((input) => {
@@ -173,37 +214,6 @@ export default class InventoryModuleService
}) })
} }
private async ensureInventoryAvailability(
data: {
allow_backorder: boolean
inventory_item_id: string
location_id: string
quantity: number
}[],
context: Context
) {
const checkLevels = data.map(async (reservation) => {
if (!!reservation.allow_backorder) {
return
}
const available = await this.retrieveAvailableQuantity(
reservation.inventory_item_id,
[reservation.location_id],
context
)
if (available < reservation.quantity) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Not enough stock available for item ${reservation.inventory_item_id} at location ${reservation.location_id}`
)
}
})
await promiseAll(checkLevels)
}
// @ts-ignore // @ts-ignore
async createReservationItems( async createReservationItems(
input: InventoryTypes.CreateReservationItemInput[], input: InventoryTypes.CreateReservationItemInput[],
@@ -225,13 +235,6 @@ export default class InventoryModuleService
InventoryTypes.ReservationItemDTO[] | InventoryTypes.ReservationItemDTO InventoryTypes.ReservationItemDTO[] | InventoryTypes.ReservationItemDTO
> { > {
const toCreate = Array.isArray(input) ? input : [input] const toCreate = Array.isArray(input) ? input : [input]
const sanitized = toCreate.map((d) => ({
...d,
allow_backorder: d.allow_backorder || false,
}))
await this.ensureInventoryAvailability(sanitized, context)
const created = await this.createReservationItems_(toCreate, context) const created = await this.createReservationItems_(toCreate, context)
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
@@ -262,10 +265,17 @@ export default class InventoryModuleService
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<ReservationItem[]> { ): Promise<ReservationItem[]> {
const inventoryLevels = await this.ensureInventoryLevels( const inventoryLevels = await this.ensureInventoryLevels(
input.map(({ location_id, inventory_item_id }) => ({ input.map(
({ location_id, inventory_item_id, quantity, allow_backorder }) => ({
location_id, location_id,
inventory_item_id, inventory_item_id,
})), quantity,
allow_backorder,
})
),
{
validateQuantityAtLocation: true,
},
context context
) )
const created = await this.reservationItemService_.create(input, context) const created = await this.reservationItemService_.create(input, context)
@@ -275,7 +285,7 @@ export default class InventoryModuleService
const locationMap = acc.get(curr.inventory_item_id) ?? new Map() const locationMap = acc.get(curr.inventory_item_id) ?? new Map()
const adjustment = locationMap.get(curr.location_id) ?? 0 const adjustment = locationMap.get(curr.location_id) ?? 0
locationMap.set(curr.location_id, adjustment + curr.quantity) locationMap.set(curr.location_id, MathBN.add(adjustment, curr.quantity))
acc.set(curr.inventory_item_id, locationMap) acc.set(curr.inventory_item_id, locationMap)
return acc return acc
@@ -294,7 +304,7 @@ export default class InventoryModuleService
return { return {
id: level.id, id: level.id,
reserved_quantity: level.reserved_quantity + adjustment, reserved_quantity: MathBN.add(level.reserved_quantity, adjustment),
} }
}) })
@@ -581,6 +591,7 @@ export default class InventoryModuleService
location_id, location_id,
inventory_item_id, inventory_item_id,
})), })),
undefined,
context context
) )
@@ -682,21 +693,6 @@ export default class InventoryModuleService
reservationItems.map((r) => [r.id, r]) reservationItems.map((r) => [r.id, r])
) )
const availabilityData = input.map((data) => {
const reservation = reservationMap.get(data.id)!
return {
...data,
quantity: data.quantity ?? reservation.quantity,
allow_backorder:
data.allow_backorder || reservation.allow_backorder || false,
inventory_item_id: reservation.inventory_item_id,
location_id: data.location_id ?? reservation.location_id,
}
})
await this.ensureInventoryAvailability(availabilityData, context)
const adjustments: Map<string, Map<string, number>> = input.reduce( const adjustments: Map<string, Map<string, number>> = input.reduce(
(acc, update) => { (acc, update) => {
const reservation = reservationMap.get(update.id)! const reservation = reservationMap.get(update.id)!
@@ -711,7 +707,7 @@ export default class InventoryModuleService
locationMap.set( locationMap.set(
reservation.location_id, reservation.location_id,
reservationLocationAdjustment - reservation.quantity MathBN.sub(reservationLocationAdjustment, reservation.quantity)
) )
const updateLocationAdjustment = const updateLocationAdjustment =
@@ -719,18 +715,24 @@ export default class InventoryModuleService
locationMap.set( locationMap.set(
update.location_id, update.location_id,
updateLocationAdjustment + (update.quantity || reservation.quantity) MathBN.add(
updateLocationAdjustment,
update.quantity || reservation.quantity
)
) )
} else if ( } else if (
isDefined(update.quantity) && isDefined(update.quantity) &&
update.quantity !== reservation.quantity !MathBN.eq(update.quantity, reservation.quantity)
) { ) {
const locationAdjustment = const locationAdjustment =
locationMap.get(reservation.location_id) ?? 0 locationMap.get(reservation.location_id) ?? 0
locationMap.set( locationMap.set(
reservation.location_id, reservation.location_id,
locationAdjustment + (update.quantity! - reservation.quantity) MathBN.add(
locationAdjustment,
MathBN.sub(update.quantity!, reservation.quantity)
)
) )
} }
@@ -740,17 +742,28 @@ export default class InventoryModuleService
}, },
new Map() new Map()
) )
const availabilityData = input.map((data) => {
const reservation = reservationMap.get(data.id)!
const result = await this.reservationItemService_.update(input, context) return {
inventory_item_id: reservation.inventory_item_id,
location_id: data.location_id ?? reservation.location_id,
quantity: data.quantity ?? reservation.quantity,
allow_backorder:
data.allow_backorder || reservation.allow_backorder || false,
}
})
const inventoryLevels = await this.ensureInventoryLevels( const inventoryLevels = await this.ensureInventoryLevels(
reservationItems.map((r) => ({ availabilityData,
inventory_item_id: r.inventory_item_id, {
location_id: r.location_id, validateQuantityAtLocation: true,
})), },
context context
) )
const result = await this.reservationItemService_.update(input, context)
const levelAdjustmentUpdates = inventoryLevels const levelAdjustmentUpdates = inventoryLevels
.map((level) => { .map((level) => {
const adjustment = adjustments const adjustment = adjustments
@@ -763,7 +776,7 @@ export default class InventoryModuleService
return { return {
id: level.id, id: level.id,
reserved_quantity: level.reserved_quantity + adjustment, reserved_quantity: MathBN.add(level.reserved_quantity, adjustment),
} }
}) })
.filter(Boolean) .filter(Boolean)
@@ -932,7 +945,7 @@ export default class InventoryModuleService
adjustInventory( adjustInventory(
inventoryItemId: string, inventoryItemId: string,
locationId: string, locationId: string,
adjustment: number, adjustment: BigNumberInput,
context: Context context: Context
): Promise<InventoryTypes.InventoryLevelDTO> ): Promise<InventoryTypes.InventoryLevelDTO>
@@ -940,7 +953,7 @@ export default class InventoryModuleService
data: { data: {
inventoryItemId: string inventoryItemId: string
locationId: string locationId: string
adjustment: number adjustment: BigNumberInput
}[], }[],
context: Context context: Context
): Promise<InventoryTypes.InventoryLevelDTO[]> ): Promise<InventoryTypes.InventoryLevelDTO[]>
@@ -950,7 +963,7 @@ export default class InventoryModuleService
async adjustInventory( async adjustInventory(
inventoryItemIdOrData: string | any, inventoryItemIdOrData: string | any,
locationId?: string | Context, locationId?: string | Context,
adjustment?: number, adjustment?: BigNumberInput,
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise< ): Promise<
InventoryTypes.InventoryLevelDTO | InventoryTypes.InventoryLevelDTO[] InventoryTypes.InventoryLevelDTO | InventoryTypes.InventoryLevelDTO[]
@@ -1000,7 +1013,7 @@ export default class InventoryModuleService
async adjustInventory_( async adjustInventory_(
inventoryItemId: string, inventoryItemId: string,
locationId: string, locationId: string,
adjustment: number, adjustment: BigNumberInput,
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<InventoryLevel> { ): Promise<InventoryLevel> {
const inventoryLevel = await this.retrieveInventoryLevelByItemAndLocation( const inventoryLevel = await this.retrieveInventoryLevelByItemAndLocation(
@@ -1012,7 +1025,10 @@ export default class InventoryModuleService
const result = await this.inventoryLevelService_.update( const result = await this.inventoryLevelService_.update(
{ {
id: inventoryLevel.id, id: inventoryLevel.id,
stocked_quantity: inventoryLevel.stocked_quantity + adjustment, stocked_quantity: MathBN.add(
inventoryLevel.stocked_quantity,
adjustment
),
}, },
context context
) )
@@ -1026,9 +1042,9 @@ export default class InventoryModuleService
locationId: string, locationId: string,
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<InventoryTypes.InventoryLevelDTO> { ): Promise<InventoryTypes.InventoryLevelDTO> {
const [inventoryLevel] = await this.listInventoryLevels( const inventoryLevel = await this.listInventoryLevels(
{ inventory_item_id: inventoryItemId, location_id: locationId }, { inventory_item_id: inventoryItemId, location_id: locationId },
{ take: 1 }, { take: null },
context context
) )
@@ -1039,7 +1055,7 @@ export default class InventoryModuleService
) )
} }
return inventoryLevel return inventoryLevel[0]
} }
/** /**
@@ -1055,9 +1071,9 @@ export default class InventoryModuleService
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<number> { ): Promise<BigNumber> {
if (locationIds.length === 0) { if (locationIds.length === 0) {
return 0 return new BigNumber(0)
} }
await this.inventoryItemService_.retrieve( await this.inventoryItemService_.retrieve(
@@ -1091,9 +1107,9 @@ export default class InventoryModuleService
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<number> { ): Promise<BigNumber> {
if (locationIds.length === 0) { if (locationIds.length === 0) {
return 0 return new BigNumber(0)
} }
// Throws if item does not exist // Throws if item does not exist
@@ -1128,7 +1144,7 @@ export default class InventoryModuleService
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<number> { ): Promise<BigNumber> {
// Throws if item does not exist // Throws if item does not exist
await this.inventoryItemService_.retrieve( await this.inventoryItemService_.retrieve(
inventoryItemId, inventoryItemId,
@@ -1139,7 +1155,7 @@ export default class InventoryModuleService
) )
if (locationIds.length === 0) { if (locationIds.length === 0) {
return 0 return new BigNumber(0)
} }
const reservedQuantity = const reservedQuantity =
@@ -1164,7 +1180,7 @@ export default class InventoryModuleService
async confirmInventory( async confirmInventory(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
quantity: number, quantity: BigNumberInput,
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<boolean> { ): Promise<boolean> {
const availableQuantity = await this.retrieveAvailableQuantity( const availableQuantity = await this.retrieveAvailableQuantity(
@@ -1172,7 +1188,7 @@ export default class InventoryModuleService
locationIds, locationIds,
context context
) )
return availableQuantity >= quantity return MathBN.gte(availableQuantity, quantity)
} }
private async adjustInventoryLevelsForReservationsDeletion( private async adjustInventoryLevelsForReservationsDeletion(
@@ -1208,6 +1224,7 @@ export default class InventoryModuleService
inventory_item_id: r.inventory_item_id, inventory_item_id: r.inventory_item_id,
location_id: r.location_id, location_id: r.location_id,
})), })),
undefined,
context context
) )
@@ -1218,8 +1235,11 @@ export default class InventoryModuleService
const inventoryLevelMap = acc.get(curr.inventory_item_id) ?? new Map() const inventoryLevelMap = acc.get(curr.inventory_item_id) ?? new Map()
const adjustment = inventoryLevelMap.has(curr.location_id) const adjustment = inventoryLevelMap.has(curr.location_id)
? inventoryLevelMap.get(curr.location_id) + curr.quantity * multiplier ? MathBN.add(
: curr.quantity * multiplier inventoryLevelMap.get(curr.location_id),
MathBN.mult(curr.quantity, multiplier)
)
: MathBN.mult(curr.quantity, multiplier)
inventoryLevelMap.set(curr.location_id, adjustment) inventoryLevelMap.set(curr.location_id, adjustment)
acc.set(curr.inventory_item_id, inventoryLevelMap) acc.set(curr.inventory_item_id, inventoryLevelMap)
@@ -1237,7 +1257,7 @@ export default class InventoryModuleService
return { return {
id: level.id, id: level.id,
reserved_quantity: level.reserved_quantity + adjustment, reserved_quantity: MathBN.add(level.reserved_quantity, adjustment),
} }
}) })