feat: Confirm inventory in create cart workflow (#6635)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)!
|
||||
|
||||
@@ -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)!
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user