Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(core-flows): reserve inventory from available location #11538

Merged
merged 9 commits into from
Feb 26, 2025
5 changes: 5 additions & 0 deletions .changeset/rotten-seahorses-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/core-flows": patch
---

chore(core-flows): reserve inventory from locations with availability
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
Modules,
PriceListStatus,
PriceListType,
remoteQueryObjectFromString,
RuleOperator,
} from "@medusajs/utils"
import {
Expand Down Expand Up @@ -844,6 +845,166 @@ medusaIntegrationTestRunner({
})
)
})

it("should complete cart reserving inventory from available locations", async () => {
const salesChannel = await scModuleService.createSalesChannels({
name: "Webshop",
})

const location = await stockLocationModule.createStockLocations({
name: "Warehouse",
})

const location2 = await stockLocationModule.createStockLocations({
name: "Side Warehouse",
})

const [product] = await productModule.createProducts([
{
title: "Test product",
variants: [
{
title: "Test variant",
},
],
},
])

const inventoryItem = await inventoryModule.createInventoryItems({
sku: "inv-1234",
})

await inventoryModule.createInventoryLevels([
{
inventory_item_id: inventoryItem.id,
location_id: location.id,
stocked_quantity: 1,
reserved_quantity: 0,
},
])

await inventoryModule.createInventoryLevels([
{
inventory_item_id: inventoryItem.id,
location_id: location2.id,
stocked_quantity: 1,
reserved_quantity: 0,
},
])

const priceSet = await pricingModule.createPriceSets({
prices: [
{
amount: 3000,
currency_code: "usd",
},
],
})

await pricingModule.createPricePreferences({
attribute: "currency_code",
value: "usd",
is_tax_inclusive: true,
})

await remoteLink.create([
{
[Modules.PRODUCT]: {
variant_id: product.variants[0].id,
},
[Modules.PRICING]: {
price_set_id: priceSet.id,
},
},
{
[Modules.SALES_CHANNEL]: {
sales_channel_id: salesChannel.id,
},
[Modules.STOCK_LOCATION]: {
stock_location_id: location.id,
},
},
{
[Modules.SALES_CHANNEL]: {
sales_channel_id: salesChannel.id,
},
[Modules.STOCK_LOCATION]: {
stock_location_id: location2.id,
},
},
{
[Modules.PRODUCT]: {
variant_id: product.variants[0].id,
},
[Modules.INVENTORY]: {
inventory_item_id: inventoryItem.id,
},
},
])

// complete 2 carts
for (let i = 1; i <= 2; i++) {
const cart = await cartModuleService.createCarts({
currency_code: "usd",
sales_channel_id: salesChannel.id,
})

await addToCartWorkflow(appContainer).run({
input: {
items: [
{
variant_id: product.variants[0].id,
quantity: 1,
requires_shipping: false,
},
],
cart_id: cart.id,
},
})

await createPaymentCollectionForCartWorkflow(appContainer).run({
input: {
cart_id: cart.id,
},
})

const [payCol] = await remoteQuery(
remoteQueryObjectFromString({
entryPoint: "cart_payment_collection",
variables: { filters: { cart_id: cart.id } },
fields: ["payment_collection_id"],
})
)

await createPaymentSessionsWorkflow(appContainer).run({
input: {
payment_collection_id: payCol.payment_collection_id,
provider_id: "pp_system_default",
context: {},
data: {},
},
})

await completeCartWorkflow(appContainer).run({
input: {
id: cart.id,
},
})
}

const reservations = await api.get(
`/admin/reservations`,
adminHeaders
)

const locations = reservations.data.reservations.map(
(r) => r.location_id
)

expect(locations).toEqual(
expect.arrayContaining([location.id, location2.id])
)
})
})

describe("UpdateCartWorkflow", () => {
Expand Down
8 changes: 8 additions & 0 deletions packages/core/core-flows/src/cart/utils/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export const completeCartFields = [
"items.variant.inventory_items.inventory_item_id",
"items.variant.inventory_items.required_quantity",
"items.variant.inventory_items.inventory.requires_shipping",
"items.variant.inventory_items.inventory.location_levels.stocked_quantity",
"items.variant.inventory_items.inventory.location_levels.reserved_quantity",
"items.variant.inventory_items.inventory.location_levels.raw_stocked_quantity",
"items.variant.inventory_items.inventory.location_levels.raw_reserved_quantity",
"items.variant.inventory_items.inventory.location_levels.stock_locations.id",
"items.variant.inventory_items.inventory.location_levels.stock_locations.name",
"items.variant.inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
Expand Down Expand Up @@ -153,6 +157,10 @@ export const productVariantsFields = [
"inventory_items.inventory_item_id",
"inventory_items.required_quantity",
"inventory_items.inventory.requires_shipping",
"inventory_items.inventory.location_levels.stocked_quantity",
"inventory_items.inventory.location_levels.reserved_quantity",
"inventory_items.inventory.location_levels.raw_stocked_quantity",
"inventory_items.inventory.location_levels.raw_reserved_quantity",
"inventory_items.inventory.location_levels.stock_locations.id",
"inventory_items.inventory.location_levels.stock_locations.name",
"inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import {
BigNumberInput,
ConfirmVariantInventoryWorkflowInputDTO,
} from "@medusajs/framework/types"
import { MedusaError, deepFlatMap } from "@medusajs/framework/utils"
import {
BigNumber,
MathBN,
MedusaError,
deepFlatMap,
} from "@medusajs/framework/utils"

interface ConfirmInventoryPreparationInput {
product_variant_inventory_items: {
Expand All @@ -21,6 +26,7 @@ interface ConfirmInventoryPreparationInput {
allow_backorder?: boolean
}[]
location_ids: string[]
stockAvailability: Map<string, Map<string, BigNumberInput>>
}

interface ConfirmInventoryItem {
Expand All @@ -38,6 +44,7 @@ export const prepareConfirmInventoryInput = (data: {
const productVariantInventoryItems = new Map<string, any>()
const stockLocationIds = new Set<string>()
const allVariants = new Map<string, any>()
const mapLocationAvailability = new Map<string, Map<string, BigNumberInput>>()
let hasSalesChannelStockLocation = false
let hasManagedInventory = false

Expand All @@ -55,7 +62,13 @@ export const prepareConfirmInventoryInput = (data: {
deepFlatMap(
data.input,
"variants.inventory_items.inventory.location_levels.stock_locations.sales_channels",
({ variants, inventory_items, stock_locations, sales_channels }) => {
({
variants,
inventory_items,
location_levels,
stock_locations,
sales_channels,
}) => {
if (!variants) {
return
}
Expand All @@ -67,6 +80,29 @@ export const prepareConfirmInventoryInput = (data: {
hasSalesChannelStockLocation = true
}

if (location_levels && inventory_items) {
const availability = MathBN.sub(
location_levels.raw_stocked_quantity ??
location_levels.stocked_quantity ??
0,
location_levels.raw_reserved_quantity ??
location_levels.reserved_quantity ??
0
)

if (!mapLocationAvailability.has(location_levels.location_id)) {
mapLocationAvailability.set(location_levels.location_id, new Map())
}

const locationMap = mapLocationAvailability.get(
location_levels.location_id
)!
locationMap.set(
inventory_items.inventory_item_id,
new BigNumber(availability)
)
}

if (stock_locations && sales_channels?.id === salesChannelId) {
stockLocationIds.add(stock_locations.id)
}
Expand Down Expand Up @@ -114,6 +150,7 @@ export const prepareConfirmInventoryInput = (data: {
productVariantInventoryItems.values()
),
location_ids: Array.from(stockLocationIds),
stockAvailability: mapLocationAvailability,
items: data.input.items,
variants: Array.from(allVariants.values()),
})
Expand All @@ -125,6 +162,7 @@ const formatInventoryInput = ({
product_variant_inventory_items,
location_ids,
items,
stockAvailability,
variants,
}: ConfirmInventoryPreparationInput) => {
if (!product_variant_inventory_items.length) {
Expand Down Expand Up @@ -156,16 +194,27 @@ const formatInventoryInput = ({
)
}

variantInventoryItems.forEach((variantInventoryItem) =>
variantInventoryItems.forEach((variantInventoryItem) => {
const locationsWithAvailability = location_ids.filter((locId) =>
MathBN.gte(
stockAvailability
.get(locId)
?.get(variantInventoryItem.inventory_item_id) ?? 0,
MathBN.mult(variantInventoryItem.required_quantity, item.quantity)
)
)

itemsToConfirm.push({
id: item.id,
inventory_item_id: variantInventoryItem.inventory_item_id,
required_quantity: variantInventoryItem.required_quantity,
allow_backorder: !!variant.allow_backorder,
quantity: item.quantity,
location_ids: location_ids,
location_ids: locationsWithAvailability.length
? locationsWithAvailability
: location_ids,
})
)
})
})

return itemsToConfirm
Expand Down
Loading
Loading