diff --git a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts index 14731c7a06..8a46ab89e1 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -18,6 +18,7 @@ import { ICartModuleService, ICustomerModuleService, IFulfillmentModuleService, + IInventoryServiceNext, IPaymentModuleService, IPricingModuleService, IProductModuleService, @@ -45,6 +46,8 @@ medusaIntegrationTestRunner({ let productModule: IProductModuleService let pricingModule: IPricingModuleService let paymentModule: IPaymentModuleService + let inventoryModule: IInventoryServiceNext + let stockLocationModule: IStockLocationServiceNext let fulfillmentModule: IFulfillmentModuleService let locationModule: IStockLocationServiceNext let remoteLink, remoteQuery @@ -64,6 +67,10 @@ medusaIntegrationTestRunner({ productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT) pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING) paymentModule = appContainer.resolve(ModuleRegistrationName.PAYMENT) + inventoryModule = appContainer.resolve(ModuleRegistrationName.INVENTORY) + stockLocationModule = appContainer.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) fulfillmentModule = appContainer.resolve( ModuleRegistrationName.FULFILLMENT ) @@ -97,6 +104,10 @@ medusaIntegrationTestRunner({ name: "Webshop", }) + const location = await stockLocationModule.create({ + name: "Warehouse", + }) + const [product] = await productModule.create([ { title: "Test product", @@ -108,6 +119,19 @@ medusaIntegrationTestRunner({ }, ]) + const inventoryItem = await inventoryModule.create({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + const priceSet = await pricingModule.create({ prices: [ { @@ -119,13 +143,29 @@ medusaIntegrationTestRunner({ await remoteLink.create([ { - productService: { + [Modules.PRODUCT]: { variant_id: product.variants[0].id, }, - pricingService: { + [Modules.PRICING]: { price_set_id: priceSet.id, }, }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, ]) const { result } = await createCartWorkflow(appContainer).run({ @@ -184,6 +224,99 @@ medusaIntegrationTestRunner({ ]) }) + it("should throw if variants are out of stock", async () => { + const salesChannel = await scModuleService.create({ + name: "Webshop", + }) + + const location = await stockLocationModule.create({ + name: "Warehouse", + }) + + const [product] = await productModule.create([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + }, + ], + }, + ]) + + const inventoryItem = await inventoryModule.create({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 2, + }, + ]) + + const priceSet = await pricingModule.create({ + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + }) + + 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.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + + const { errors } = await createCartWorkflow(appContainer).run({ + input: { + sales_channel_id: salesChannel.id, + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + }, + throwOnError: false, + }) + + expect(errors).toEqual([ + { + action: "confirm-inventory-step", + handlerType: "invoke", + error: new Error( + "Some variant does not have the required inventory" + ), + }, + ]) + }) + it("should throw if sales channel is disabled", async () => { const salesChannel = await scModuleService.create({ name: "Webshop", @@ -283,8 +416,17 @@ medusaIntegrationTestRunner({ describe("AddToCartWorkflow", () => { it("should add item to cart", async () => { + const salesChannel = await scModuleService.create({ + name: "Webshop", + }) + + const location = await stockLocationModule.create({ + name: "Warehouse", + }) + let cart = await cartModuleService.create({ currency_code: "usd", + sales_channel_id: salesChannel.id, }) const [product] = await productModule.create([ @@ -298,6 +440,19 @@ medusaIntegrationTestRunner({ }, ]) + const inventoryItem = await inventoryModule.create({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + const priceSet = await pricingModule.create({ prices: [ { @@ -309,13 +464,29 @@ medusaIntegrationTestRunner({ await remoteLink.create([ { - productService: { + [Modules.PRODUCT]: { variant_id: product.variants[0].id, }, - pricingService: { + [Modules.PRICING]: { price_set_id: priceSet.id, }, }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, ]) cart = await cartModuleService.retrieve(cart.id, { @@ -354,8 +525,17 @@ medusaIntegrationTestRunner({ }) it("should throw if no price sets for variant exist", async () => { - const cart = await cartModuleService.create({ + const salesChannel = await scModuleService.create({ + name: "Webshop", + }) + + const location = await stockLocationModule.create({ + name: "Warehouse", + }) + + let cart = await cartModuleService.create({ currency_code: "usd", + sales_channel_id: salesChannel.id, }) const [product] = await productModule.create([ @@ -369,6 +549,38 @@ medusaIntegrationTestRunner({ }, ]) + const inventoryItem = await inventoryModule.create({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + + await remoteLink.create([ + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + const { errors } = await addToCartWorkflow(appContainer).run({ input: { items: [ @@ -423,6 +635,14 @@ medusaIntegrationTestRunner({ describe("updateLineItemInCartWorkflow", () => { it("should update item in cart", async () => { + const salesChannel = await scModuleService.create({ + name: "Webshop", + }) + + const location = await stockLocationModule.create({ + name: "Warehouse", + }) + const [product] = await productModule.create([ { title: "Test product", @@ -434,6 +654,19 @@ medusaIntegrationTestRunner({ }, ]) + const inventoryItem = await inventoryModule.create({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + const priceSet = await pricingModule.create({ prices: [ { @@ -445,17 +678,34 @@ medusaIntegrationTestRunner({ await remoteLink.create([ { - productService: { + [Modules.PRODUCT]: { variant_id: product.variants[0].id, }, - pricingService: { + [Modules.PRICING]: { price_set_id: priceSet.id, }, }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, ]) let cart = await cartModuleService.create({ currency_code: "usd", + sales_channel_id: salesChannel.id, items: [ { variant_id: product.variants[0].id, @@ -473,7 +723,9 @@ medusaIntegrationTestRunner({ const item = cart.items?.[0]! - await updateLineItemInCartWorkflow(appContainer).run({ + const { errors } = await updateLineItemInCartWorkflow( + appContainer + ).run({ input: { cart, item, @@ -510,6 +762,14 @@ medusaIntegrationTestRunner({ }, }) + const salesChannel = await scModuleService.create({ + name: "Webshop", + }) + + const location = await stockLocationModule.create({ + name: "Warehouse", + }) + const [product] = await productModule.create([ { title: "Test product", @@ -521,6 +781,55 @@ medusaIntegrationTestRunner({ }, ]) + const inventoryItem = await inventoryModule.create({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + + const priceSet = await pricingModule.create({ + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + }) + + 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.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + let cart = await cartModuleService.create({ currency_code: "usd", items: [ @@ -533,26 +842,6 @@ medusaIntegrationTestRunner({ ], }) - const priceSet = await pricingModule.create({ - prices: [ - { - amount: 5000, - currency_code: "usd", - }, - ], - }) - - await remoteLink.create([ - { - productService: { - variant_id: product.variants[0].id, - }, - pricingService: { - price_set_id: priceSet.id, - }, - }, - ]) - cart = await cartModuleService.retrieve(cart.id, { select: ["id", "region_id", "currency_code"], relations: ["items", "items.variant_id", "items.metadata"], diff --git a/integration-tests/modules/__tests__/cart/store/carts.spec.ts b/integration-tests/modules/__tests__/cart/store/carts.spec.ts index ad708fee7a..eccc321013 100644 --- a/integration-tests/modules/__tests__/cart/store/carts.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/carts.spec.ts @@ -80,9 +80,11 @@ medusaIntegrationTestRunner({ title: "Test product", variants: [ { + manage_inventory: false, title: "Test variant", }, { + manage_inventory: false, title: "Test variant 2", }, ], @@ -178,10 +180,16 @@ medusaIntegrationTestRunner({ const [product] = await productModule.create([ { title: "Test product default tax", - variants: [{ title: "Test variant default tax" }], + variants: [ + { title: "Test variant default tax", manage_inventory: false }, + ], }, ]) + const salesChannel = await scModule.create({ + name: "Webshop", + }) + const [priceSet] = await pricingModule.create([ { prices: [{ amount: 3000, currency_code: "usd" }] }, ]) @@ -204,6 +212,7 @@ medusaIntegrationTestRunner({ province: "NY", postal_code: "94016", }, + sales_channel_id: salesChannel.id, items: [ { quantity: 1, @@ -789,25 +798,32 @@ medusaIntegrationTestRunner({ email: "tony@stark-industries.com", }) + const salesChannel = await scModule.create({ + name: "Webshop", + }) + const [productWithSpecialTax] = await productModule.create([ { // This product ID is setup in the tax structure fixture (setupTaxStructure) id: "product_id_1", title: "Test product", - variants: [{ title: "Test variant" }], + variants: [{ title: "Test variant", manage_inventory: false }], } as any, ]) const [productWithDefaultTax] = await productModule.create([ { title: "Test product default tax", - variants: [{ title: "Test variant default tax" }], + variants: [ + { title: "Test variant default tax", manage_inventory: false }, + ], }, ]) const cart = await cartModule.create({ currency_code: "usd", customer_id: customer.id, + sales_channel_id: salesChannel.id, region_id: region.id, shipping_address: { customer_id: customer.id, diff --git a/packages/core-flows/src/definition/cart/steps/confirm-inventory.ts b/packages/core-flows/src/definition/cart/steps/confirm-inventory.ts new file mode 100644 index 0000000000..3ec730a396 --- /dev/null +++ b/packages/core-flows/src/definition/cart/steps/confirm-inventory.ts @@ -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( + 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) + } +) diff --git a/packages/core-flows/src/definition/cart/steps/index.ts b/packages/core-flows/src/definition/cart/steps/index.ts index e81f09ba55..2ae250b5eb 100644 --- a/packages/core-flows/src/definition/cart/steps/index.ts +++ b/packages/core-flows/src/definition/cart/steps/index.ts @@ -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" diff --git a/packages/core-flows/src/definition/cart/utils/prepare-confirm-inventory-input.ts b/packages/core-flows/src/definition/cart/utils/prepare-confirm-inventory-input.ts new file mode 100644 index 0000000000..ae908b5f66 --- /dev/null +++ b/packages/core-flows/src/definition/cart/utils/prepare-confirm-inventory-input.ts @@ -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 +} diff --git a/packages/core-flows/src/definition/cart/workflows/add-to-cart.ts b/packages/core-flows/src/definition/cart/workflows/add-to-cart.ts index 87dda4ff24..a4b0dcb7ca 100644 --- a/packages/core-flows/src/definition/cart/workflows/add-to-cart.ts +++ b/packages/core-flows/src/definition/cart/workflows/add-to-cart.ts @@ -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) => { - 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)! diff --git a/packages/core-flows/src/definition/cart/workflows/create-carts.ts b/packages/core-flows/src/definition/cart/workflows/create-carts.ts index a502bdcb98..c85c511ab4 100644 --- a/packages/core-flows/src/definition/cart/workflows/create-carts.ts +++ b/packages/core-flows/src/definition/cart/workflows/create-carts.ts @@ -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)! diff --git a/packages/core-flows/src/definition/cart/workflows/update-line-item-in-cart.ts b/packages/core-flows/src/definition/cart/workflows/update-line-item-in-cart.ts index 529fbcd91c..b26324adc4 100644 --- a/packages/core-flows/src/definition/cart/workflows/update-line-item-in-cart.ts +++ b/packages/core-flows/src/definition/cart/workflows/update-line-item-in-cart.ts @@ -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, diff --git a/packages/inventory-next/src/services/inventory.ts b/packages/inventory-next/src/services/inventory.ts index 14fce92648..997a14c1ab 100644 --- a/packages/inventory-next/src/services/inventory.ts +++ b/packages/inventory-next/src/services/inventory.ts @@ -1,29 +1,29 @@ import { InternalModuleDeclaration } from "@medusajs/modules-sdk" import { Context, + DAL, IInventoryServiceNext, + InventoryNext, InventoryTypes, ModuleJoinerConfig, ModulesSdkTypes, - InventoryNext, ReservationItemDTO, } from "@medusajs/types" import { + CommonEvents, EmitEvents, + InjectManager, + InjectTransactionManager, + InventoryEvents, MedusaContext, MedusaError, ModulesSdkUtils, + isDefined, + partitionArray, } from "@medusajs/utils" -import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { InventoryItem, InventoryLevel, ReservationItem } from "@models" -import { DAL } from "@medusajs/types" -import { InjectTransactionManager } from "@medusajs/utils" -import { InjectManager } from "@medusajs/utils" +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import InventoryLevelService from "./inventory-level" -import { partitionArray } from "@medusajs/utils" -import { InventoryEvents } from "@medusajs/utils" -import { CommonEvents } from "@medusajs/utils" -import { isDefined } from "@medusajs/utils" type InjectedDependencies = { baseRepository: DAL.RepositoryService diff --git a/packages/link-modules/src/definitions/sales-channel-location.ts b/packages/link-modules/src/definitions/sales-channel-location.ts index 77e9736cd1..33bf20f03c 100644 --- a/packages/link-modules/src/definitions/sales-channel-location.ts +++ b/packages/link-modules/src/definitions/sales-channel-location.ts @@ -6,7 +6,7 @@ export const SalesChannelLocation: ModuleJoinerConfig = { serviceName: LINKS.SalesChannelLocation, isLink: true, databaseConfig: { - tableName: "sales_channel_locations", + tableName: "sales_channel_stock_location", idPrefix: "scloc", }, alias: [ diff --git a/packages/stock-location-next/src/module-definition.ts b/packages/stock-location-next/src/module-definition.ts index ddeba045bd..aacce844a9 100644 --- a/packages/stock-location-next/src/module-definition.ts +++ b/packages/stock-location-next/src/module-definition.ts @@ -2,8 +2,8 @@ import * as StockLocationModels from "@models" import * as StockLocationRepostiories from "@repositories" import * as StockLocationServices from "@services" -import { ModuleExports } from "@medusajs/types" import { Modules } from "@medusajs/modules-sdk" +import { ModuleExports } from "@medusajs/types" import { ModulesSdkUtils } from "@medusajs/utils" import { StockLocationModuleService } from "@services"