fix(medusa): calculate sales channel availability correctly for variants (#10448)

* fix: calculate inventory quantities based on sales channel and locations

* Update packages/medusa/src/api/utils/middlewares/products/variant-inventory-quantity.ts

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>

* Update packages/medusa/src/api/utils/middlewares/products/variant-inventory-quantity.ts

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>

* Update packages/core/core-flows/src/product/steps/get-variant-availability.ts

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>

* fix: crk

---------

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Sebastian Rindom
2024-12-05 19:29:47 +01:00
committed by GitHub
parent b160fd3cbf
commit 7ff3f15d6d
11 changed files with 418 additions and 121 deletions

View File

@@ -0,0 +1,178 @@
import { RemoteQueryFunction } from "@medusajs/types"
/**
* Computes the varaint availability for a list of variants in a given sales channel
*
* The availability algorithm works as follows:
* 1. For each variant, we retrieve its inventory items.
* 2. We calculate the available quantity for each inventory item, considering only the stock locations associated with the given sales channel.
* 3. For each inventory item, we calculate the maximum deliverable quantity by dividing the available quantity by the quantity required for the variant.
* 4. We take the minimum of these maximum deliverable quantities across all inventory items for the variant.
* 5. This minimum value represents the overall availability of the variant in the given sales channel.
*
* The algorithm takes into account:
* - Variant inventory items: The inventory records associated with each variant.
* - Required quantities: The quantity of each inventory item required to fulfill one unit of the variant.
* - Sales channels: The specific sales channel for which we're calculating availability.
* - Stock locations: The inventory locations associated with the sales channel.
*
* @param query - The Query function
* @param data - An object containing the variant ids and the sales channel id to compute the availability for
* @returns an object containing the variant ids and their availability
*/
export async function getVariantAvailability(
query: Omit<RemoteQueryFunction, symbol>,
data: VariantAvailabilityData
): Promise<{
[variant_id: string]: {
availability: number
sales_channel_id: string
}
}> {
const { variantInventoriesMap, locationIds } = await getDataForComputation(
query,
data
)
return data.variant_ids.reduce((acc, variantId) => {
const variantInventoryItems = variantInventoriesMap.get(variantId) || []
acc[variantId] = {
availability: computeVariantAvailability(
variantInventoryItems,
locationIds,
{ requireChannelCheck: true }
),
sales_channel_id: data.sales_channel_id,
}
return acc
}, {})
}
type TotalVariantAvailabilityData = {
variant_ids: string[]
}
/**
* Computes the total availability for a list of variants across all stock locations
*
* @param query - The Query function
* @param data - An object containing the variant ids to compute the availability for
* @returns the total availability for the given variants
*/
export async function getTotalVariantAvailability(
query: Omit<RemoteQueryFunction, symbol>,
data: TotalVariantAvailabilityData
): Promise<{
[variant_id: string]: {
availability: number
}
}> {
const { variantInventoriesMap, locationIds } = await getDataForComputation(
query,
data
)
return data.variant_ids.reduce((acc, variantId) => {
const variantInventoryItems = variantInventoriesMap.get(variantId) || []
acc[variantId] = {
availability: computeVariantAvailability(
variantInventoryItems,
locationIds,
{ requireChannelCheck: false }
),
}
return acc
}, {})
}
interface VariantItems {
variant_id: string
required_quantity: number
variant: {
manage_inventory: boolean
allow_backorder: boolean
}
inventory: {
location_levels: {
location_id: string
available_quantity: number
}[]
}
}
const computeVariantAvailability = (
variantInventoryItems: VariantItems[],
channelLocationsSet: Set<string>,
{ requireChannelCheck } = { requireChannelCheck: true }
) => {
const inventoryQuantities: number[] = []
for (const link of variantInventoryItems) {
const requiredQuantity = link.required_quantity
const availableQuantity = (link.inventory?.location_levels || []).reduce(
(sum, level) => {
if (
requireChannelCheck &&
!channelLocationsSet.has(level.location_id)
) {
return sum
}
return sum + (level?.available_quantity || 0)
},
0
)
// This will give us the maximum deliverable quantities for each inventory item
const maxInventoryQuantity = Math.floor(
availableQuantity / requiredQuantity
)
inventoryQuantities.push(maxInventoryQuantity)
}
return inventoryQuantities.length ? Math.min(...inventoryQuantities) : 0
}
type VariantAvailabilityData = {
variant_ids: string[]
sales_channel_id: string
}
const getDataForComputation = async (
query: Omit<RemoteQueryFunction, symbol>,
data: { variant_ids: string[]; sales_channel_id?: string }
) => {
const { data: variantInventoryItems } = await query.graph({
entity: "product_variant_inventory_items",
fields: [
"variant_id",
"required_quantity",
"variant.manage_inventory",
"variant.allow_backorder",
"inventory.*",
"inventory.location_levels.*",
],
filters: { variant_id: data.variant_ids },
})
const variantInventoriesMap = new Map()
variantInventoryItems.forEach((link) => {
const array = variantInventoriesMap.get(link.variant_id) || []
array.push(link)
variantInventoriesMap.set(link.variant_id, array)
})
const locationIds = new Set<string>()
if (data.sales_channel_id) {
const { data: channelLocations } = await query.graph({
entity: "sales_channel_locations",
fields: ["stock_location_id"],
filters: { sales_channel_id: data.sales_channel_id },
})
channelLocations.forEach((loc) => locationIds.add(loc.stock_location_id))
}
return { variantInventoriesMap, locationIds }
}

View File

@@ -6,3 +6,4 @@ export enum ProductStatus {
}
export * from "./events"
export * from "./get-variant-availability"