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