feat: Confirm inventory in create cart workflow (#6635)

This commit is contained in:
Oli Juhl
2024-03-13 18:24:08 +01:00
committed by GitHub
parent b78f863d80
commit 02e784ce78
11 changed files with 652 additions and 102 deletions

View File

@@ -0,0 +1,47 @@
import { IInventoryService } from "@medusajs/types"
import { promiseAll } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
import { MedusaError } from "medusa-core-utils"
import { ModuleRegistrationName } from "../../../../../modules-sdk/dist"
interface StepInput {
items: {
inventory_item_id: string
required_quantity: number
quantity: number
location_ids: string[]
}[]
}
export const confirmInventoryStepId = "confirm-inventory-step"
export const confirmInventoryStep = createStep(
confirmInventoryStepId,
async (data: StepInput, { container }) => {
const inventoryService = container.resolve<IInventoryService>(
ModuleRegistrationName.INVENTORY
)
// TODO: Should be bulk
const promises = data.items.map((item) => {
const itemQuantity = item.required_quantity * item.quantity
return inventoryService.confirmInventory(
item.inventory_item_id,
item.location_ids,
itemQuantity
)
})
const inventoryCoverage = await promiseAll(promises)
if (inventoryCoverage.some((hasCoverage) => !hasCoverage)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Some variant does not have the required inventory`,
MedusaError.Codes.INSUFFICIENT_INVENTORY
)
}
return new StepResponse(null)
}
)

View File

@@ -1,5 +1,6 @@
export * from "./add-shipping-method-to-cart"
export * from "./add-to-cart"
export * from "./confirm-inventory"
export * from "./create-carts"
export * from "./create-line-item-adjustments"
export * from "./create-shipping-method-adjustments"

View File

@@ -0,0 +1,63 @@
import { MedusaError } from "medusa-core-utils"
interface ConfirmInventoryPreparationInput {
product_variant_inventory_items: {
variant_id: string
inventory_item_id: string
required_quantity: number
}[]
items: { variant_id?: string; quantity: number }[]
variants: { id: string; manage_inventory?: boolean }[]
location_ids: string[]
}
interface ConfirmInventoryItem {
inventory_item_id: string
required_quantity: number
quantity: number
location_ids: string[]
}
export const prepareConfirmInventoryInput = ({
product_variant_inventory_items,
location_ids,
items,
variants,
}: ConfirmInventoryPreparationInput) => {
if (!product_variant_inventory_items.length) {
return []
}
const variantsMap = new Map<
string,
{ id: string; manage_inventory?: boolean }
>(variants.map((v) => [v.id, v]))
const itemsToConfirm: ConfirmInventoryItem[] = []
items.forEach((item) => {
const variantInventoryItem = product_variant_inventory_items.find(
(i) => i.variant_id === item.variant_id
)
if (!variantInventoryItem) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Variant ${item.variant_id} does not have any inventory items associated with it.`
)
}
const variant = variantsMap.get(item.variant_id!)
if (variant?.manage_inventory) {
itemsToConfirm.push({
inventory_item_id: variantInventoryItem.inventory_item_id,
required_quantity: variantInventoryItem.required_quantity,
quantity: item.quantity,
location_ids: location_ids,
})
}
})
return itemsToConfirm
}

View File

@@ -7,31 +7,95 @@ import {
createWorkflow,
transform,
} from "@medusajs/workflows-sdk"
import { MedusaError } from "medusa-core-utils"
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
import {
addToCartStep,
confirmInventoryStep,
getVariantPriceSetsStep,
getVariantsStep,
validateVariantsExistStep,
} from "../steps"
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
import { updateTaxLinesStep } from "../steps/update-tax-lines"
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
import { prepareLineItemData } from "../utils/prepare-line-item-data"
import { refreshPaymentCollectionForCartStep } from "./refresh-payment-collection"
// TODO: The AddToCartWorkflow are missing the following steps:
// - Confirm inventory exists (inventory module)
// - Refresh/delete shipping methods (fulfillment module)
export const addToCartWorkflowId = "add-to-cart"
export const addToCartWorkflow = createWorkflow(
addToCartWorkflowId,
(input: WorkflowData<AddToCartWorkflowInputDTO>) => {
const variantIds = validateVariantsExistStep({
variantIds: transform({ input }, (data) => {
return (data.input.items ?? []).map((i) => i.variant_id)
}),
const variantIds = transform({ input }, (data) => {
return (data.input.items ?? []).map((i) => i.variant_id)
})
validateVariantsExistStep({ variantIds })
const variants = getVariantsStep({
filter: { id: variantIds },
config: {
select: [
"id",
"title",
"sku",
"barcode",
"product.id",
"product.title",
"product.description",
"product.subtitle",
"product.thumbnail",
"product.type",
"product.collection",
"product.handle",
],
relations: ["product"],
},
})
const salesChannelLocations = useRemoteQueryStep({
entry_point: "sales_channels",
fields: ["id", "name", "stock_locations.id", "stock_locations.name"],
variables: { id: input.cart.sales_channel_id },
})
const productVariantInventoryItems = useRemoteQueryStep({
entry_point: "product_variant_inventory_items",
fields: ["variant_id", "inventory_item_id", "required_quantity"],
variables: { variant_id: variantIds },
}).config({ name: "inventory-items" })
const confirmInventoryInput = transform(
{ productVariantInventoryItems, salesChannelLocations, input, variants },
(data) => {
if (!data.salesChannelLocations.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Sales channel ${data.input.cart.sales_channel_id} is not associated with any stock locations.`
)
}
const items = prepareConfirmInventoryInput({
product_variant_inventory_items: data.productVariantInventoryItems,
location_ids: data.salesChannelLocations[0].stock_locations.map(
(l) => l.id
),
items: data.input.items!,
variants: data.variants.map((v) => ({
id: v.id,
manage_inventory: v.manage_inventory,
})),
})
return { items }
}
)
confirmInventoryStep(confirmInventoryInput)
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
const pricingContext = transform({ cart: input.cart }, (data) => {
return {
@@ -46,31 +110,6 @@ export const addToCartWorkflow = createWorkflow(
context: pricingContext,
})
const variants = getVariantsStep(
transform({ variantIds }, (data) => {
return {
filter: { id: data.variantIds },
config: {
select: [
"id",
"title",
"sku",
"barcode",
"product.id",
"product.title",
"product.description",
"product.subtitle",
"product.thumbnail",
"product.type",
"product.collection",
"product.handle",
],
relations: ["product"],
},
}
})
)
const lineItems = transform({ priceSets, input, variants }, (data) => {
const items = (data.input.items ?? []).map((item) => {
const variant = data.variants.find((v) => v.id === item.variant_id)!

View File

@@ -5,7 +5,10 @@ import {
parallelize,
transform,
} from "@medusajs/workflows-sdk"
import { MedusaError } from "medusa-core-utils"
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
import {
confirmInventoryStep,
createCartsStep,
findOneOrAnyRegionStep,
findOrCreateCustomerStep,
@@ -16,11 +19,11 @@ import {
} from "../steps"
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
import { updateTaxLinesStep } from "../steps/update-tax-lines"
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
import { prepareLineItemData } from "../utils/prepare-line-item-data"
import { refreshPaymentCollectionForCartStep } from "./refresh-payment-collection"
// TODO: The UpdateLineItemsWorkflow are missing the following steps:
// - Confirm inventory exists (inventory module)
// TODO: The createCartWorkflow are missing the following steps:
// - Refresh/delete shipping methods (fulfillment module)
export const createCartWorkflowId = "create-cart"
@@ -45,6 +48,73 @@ export const createCartWorkflow = createWorkflow(
validateVariantsExistStep({ variantIds })
)
const variants = getVariantsStep({
filter: { id: variantIds },
config: {
select: [
"id",
"title",
"sku",
"manage_inventory",
"barcode",
"product.id",
"product.title",
"product.description",
"product.subtitle",
"product.thumbnail",
"product.type",
"product.collection",
"product.handle",
],
relations: ["product"],
},
})
const salesChannelLocations = useRemoteQueryStep({
entry_point: "sales_channels",
fields: ["id", "name", "stock_locations.id", "stock_locations.name"],
variables: { id: salesChannel.id },
})
const productVariantInventoryItems = useRemoteQueryStep({
entry_point: "product_variant_inventory_items",
fields: ["variant_id", "inventory_item_id", "required_quantity"],
variables: { variant_id: variantIds },
}).config({ name: "inventory-items" })
const confirmInventoryInput = transform(
{ productVariantInventoryItems, salesChannelLocations, input, variants },
(data) => {
// We don't want to confirm inventory if there are no items in the cart.
if (!data.input.items) {
return { items: [] }
}
if (!data.salesChannelLocations.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Sales channel ${data.input.sales_channel_id} is not associated with any stock locations.`
)
}
const items = prepareConfirmInventoryInput({
product_variant_inventory_items: data.productVariantInventoryItems,
location_ids: data.salesChannelLocations[0].stock_locations.map(
(l) => l.id
),
items: data.input.items!,
variants: data.variants.map((v) => ({
id: v.id,
manage_inventory: v.manage_inventory,
})),
})
return { items }
}
)
confirmInventoryStep(confirmInventoryInput)
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
const pricingContext = transform(
{ input, region, customerData },
@@ -84,31 +154,6 @@ export const createCartWorkflow = createWorkflow(
}
)
const variants = getVariantsStep(
transform({ variantIds }, (data) => {
return {
filter: { id: data.variantIds },
config: {
select: [
"id",
"title",
"sku",
"barcode",
"product.id",
"product.title",
"product.description",
"product.subtitle",
"product.thumbnail",
"product.type",
"product.collection",
"product.handle",
],
relations: ["product"],
},
}
})
)
const lineItems = transform({ priceSets, input, variants }, (data) => {
const items = (data.input.items ?? []).map((item) => {
const variant = data.variants.find((v) => v.id === item.variant_id)!

View File

@@ -4,15 +4,20 @@ import {
createWorkflow,
transform,
} from "@medusajs/workflows-sdk"
import { MedusaError } from "medusa-core-utils"
import {
confirmInventoryStep,
getVariantPriceSetsStep,
getVariantsStep,
} from ".."
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
import { updateLineItemsStep } from "../../line-item/steps"
import { getVariantPriceSetsStep } from "../steps"
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
import { refreshPaymentCollectionForCartStep } from "./refresh-payment-collection"
// TODO: The UpdateLineItemsWorkflow are missing the following steps:
// - Confirm inventory exists (inventory module)
// - Validate shipping methods for new items (fulfillment module)
// - Refresh line item adjustments (promotion module)
export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
export const updateLineItemInCartWorkflow = createWorkflow(
@@ -32,6 +37,51 @@ export const updateLineItemInCartWorkflow = createWorkflow(
data.input.item.variant_id!,
])
const salesChannelLocations = useRemoteQueryStep({
entry_point: "sales_channels",
fields: ["id", "name", "stock_locations.id", "stock_locations.name"],
variables: { id: input.cart.sales_channel_id },
})
const productVariantInventoryItems = useRemoteQueryStep({
entry_point: "product_variant_inventory_items",
fields: ["variant_id", "inventory_item_id", "required_quantity"],
variables: { variant_id: variantIds },
}).config({ name: "inventory-items" })
const variants = getVariantsStep({
filter: { id: variantIds },
config: { select: ["id", "manage_inventory"] },
})
const confirmInventoryInput = transform(
{ productVariantInventoryItems, salesChannelLocations, input, variants },
(data) => {
if (!data.salesChannelLocations.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Sales channel ${data.input.cart.sales_channel_id} is not associated with any stock locations.`
)
}
const items = prepareConfirmInventoryInput({
product_variant_inventory_items: data.productVariantInventoryItems,
location_ids: data.salesChannelLocations[0].stock_locations.map(
(l) => l.id
),
items: [data.input.item],
variants: data.variants.map((v) => ({
id: v.id,
manage_inventory: v.manage_inventory,
})),
})
return { items }
}
)
confirmInventoryStep(confirmInventoryInput)
const priceSets = getVariantPriceSetsStep({
variantIds,
context: pricingContext,