diff --git a/integration-tests/http/__tests__/product/admin/product.spec.ts b/integration-tests/http/__tests__/product/admin/product.spec.ts index 60b4988e96..9cd6aba834 100644 --- a/integration-tests/http/__tests__/product/admin/product.spec.ts +++ b/integration-tests/http/__tests__/product/admin/product.spec.ts @@ -1744,6 +1744,165 @@ medusaIntegrationTestRunner({ }) ) }) + + it("creates product with variant inventory kits", async () => { + const inventoryItem1 = ( + await api.post( + `/admin/inventory-items`, + { sku: "inventory-1" }, + adminHeaders + ) + ).data.inventory_item + + const inventoryItem2 = ( + await api.post( + `/admin/inventory-items`, + { sku: "inventory-2" }, + adminHeaders + ) + ).data.inventory_item + + const inventoryItem3 = ( + await api.post( + `/admin/inventory-items`, + { sku: "inventory-3" }, + adminHeaders + ) + ).data.inventory_item + + const payload = { + title: "Test product - 1", + handle: "test-1", + variants: [ + { + title: "Custom inventory 1", + prices: [{ currency_code: "usd", amount: 100 }], + manage_inventory: true, + inventory_items: [ + { + inventory_item_id: inventoryItem1.id, + required_quantity: 4, + }, + ], + }, + { + title: "No inventory", + prices: [{ currency_code: "usd", amount: 100 }], + manage_inventory: false, + }, + { + title: "Default Inventory", + prices: [{ currency_code: "usd", amount: 100 }], + manage_inventory: true, + }, + { + title: "Custom inventory 2", + prices: [{ currency_code: "usd", amount: 100 }], + manage_inventory: true, + inventory_items: [ + { + inventory_item_id: inventoryItem2.id, + required_quantity: 5, + }, + { + inventory_item_id: inventoryItem3.id, + required_quantity: 6, + }, + ], + }, + ], + } + + const response = await api + .post( + "/admin/products?fields=%2bvariants.inventory_items.inventory.*,%2bvariants.inventory_items.*", + payload, + adminHeaders + ) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + title: "Test product - 1", + variants: expect.arrayContaining([ + expect.objectContaining({ + title: "Custom inventory 1", + manage_inventory: true, + inventory_items: [ + expect.objectContaining({ + required_quantity: 4, + inventory_item_id: inventoryItem1.id, + }), + ], + }), + expect.objectContaining({ + title: "No inventory", + manage_inventory: false, + inventory_items: [], + }), + expect.objectContaining({ + title: "Default Inventory", + manage_inventory: true, + inventory_items: [ + expect.objectContaining({ + required_quantity: 1, + inventory_item_id: expect.any(String), + }), + ], + }), + expect.objectContaining({ + title: "Custom inventory 2", + manage_inventory: true, + inventory_items: expect.arrayContaining([ + expect.objectContaining({ + required_quantity: 5, + inventory_item_id: inventoryItem2.id, + }), + expect.objectContaining({ + required_quantity: 6, + inventory_item_id: inventoryItem3.id, + }), + ]), + }), + ]), + }) + ) + }) + + it("should throw an error when inventory item does not exist", async () => { + const payload = { + title: "Test product - 1", + handle: "test-1", + variants: [ + { + title: "Custom inventory 1", + prices: [{ currency_code: "usd", amount: 100 }], + manage_inventory: true, + inventory_items: [ + { + inventory_item_id: "does-not-exist", + required_quantity: 4, + }, + ], + }, + ], + } + + const error = await api + .post("/admin/products", payload, adminHeaders) + .catch((err) => err) + + expect(error.response.status).toEqual(400) + expect(error.response.data).toEqual( + expect.objectContaining({ + type: "invalid_data", + message: "Inventory Items with ids: does-not-exist was not found", + }) + ) + }) }) describe("DELETE /admin/products/:id/options/:option_id", () => { @@ -2908,11 +3067,14 @@ medusaIntegrationTestRunner({ ) expect(res.status).toEqual(200) - expect(res.data.variant.inventory_items[0]).toEqual( - expect.objectContaining({ - required_quantity: 5, - inventory_item_id: inventoryItem.id, - }) + expect(res.data.variant.inventory_items).toHaveLength(2) + expect(res.data.variant.inventory_items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + required_quantity: 5, + inventory_item_id: inventoryItem.id, + }), + ]) ) }) }) @@ -2963,13 +3125,15 @@ medusaIntegrationTestRunner({ ) expect(res.status).toEqual(200) - expect(res.data.variant.inventory_items) - expect(res.data.variant.inventory_items).toEqual([ - expect.objectContaining({ - required_quantity: 10, - inventory_item_id: inventoryItem.id, - }), - ]) + expect(res.data.variant.inventory_items).toHaveLength(2) + expect(res.data.variant.inventory_items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + required_quantity: 10, + inventory_item_id: inventoryItem.id, + }), + ]) + ) }) }) @@ -3008,8 +3172,10 @@ medusaIntegrationTestRunner({ ) expect(res.status).toEqual(200) - expect(res.data.parent.inventory_items) - expect(res.data.parent.inventory_items).toEqual([]) + expect(res.data.parent.inventory_items).toHaveLength(1) + expect(res.data.parent.inventory_items[0].id).not.toBe( + inventoryItem.id + ) }) }) @@ -3131,11 +3297,14 @@ medusaIntegrationTestRunner({ ) ).data.variant - expect(createdLinkVariant.inventory_items[0]).toEqual( - expect.objectContaining({ - required_quantity: 15, - inventory_item_id: inventoryItemToCreate.id, - }) + expect(createdLinkVariant.inventory_items).toHaveLength(2) + expect(createdLinkVariant.inventory_items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + required_quantity: 15, + inventory_item_id: inventoryItemToCreate.id, + }), + ]) ) const updatedLinkVariant = ( @@ -3145,11 +3314,14 @@ medusaIntegrationTestRunner({ ) ).data.variant - expect(updatedLinkVariant.inventory_items[0]).toEqual( - expect.objectContaining({ - required_quantity: 25, - inventory_item_id: inventoryItemToUpdate.id, - }) + expect(updatedLinkVariant.inventory_items).toHaveLength(2) + expect(updatedLinkVariant.inventory_items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + required_quantity: 25, + inventory_item_id: inventoryItemToUpdate.id, + }), + ]) ) const deletedLinkVariant = ( @@ -3159,7 +3331,10 @@ medusaIntegrationTestRunner({ ) ).data.variant - expect(deletedLinkVariant.inventory_items).toHaveLength(0) + expect(deletedLinkVariant.inventory_items).toHaveLength(1) + expect(deletedLinkVariant.inventory_items[0].id).not.toEqual( + inventoryItemToDelete.id + ) }) }) }) diff --git a/integration-tests/modules/__tests__/cart/store/carts.spec.ts b/integration-tests/modules/__tests__/cart/store/carts.spec.ts index f0c4e62d2d..ed02aeea46 100644 --- a/integration-tests/modules/__tests__/cart/store/carts.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/carts.spec.ts @@ -1823,36 +1823,6 @@ medusaIntegrationTestRunner({ ) ).data.sales_channel - product = ( - await api.post( - "/admin/products", - { - title: "Test fixture", - tags: [{ value: "123" }, { value: "456" }], - options: [ - { title: "size", values: ["large", "small"] }, - { title: "color", values: ["green"] }, - ], - variants: [ - { - title: "Test variant", - prices: [ - { - currency_code: "usd", - amount: 100, - }, - ], - options: { - size: "large", - color: "green", - }, - }, - ], - }, - adminHeaders - ) - ).data.product - stockLocation = ( await api.post( `/admin/stock-locations`, @@ -1902,20 +1872,41 @@ medusaIntegrationTestRunner({ ], }) - await remoteLinkService.create([ - { - [Modules.STOCK_LOCATION]: { stock_location_id: stockLocation.id }, - [Modules.FULFILLMENT]: { fulfillment_set_id: fulfillmentSet.id }, - }, - { - [Modules.PRODUCT]: { - variant_id: product.variants[0].id, + product = ( + await api.post( + "/admin/products", + { + title: "Test fixture", + tags: [{ value: "123" }, { value: "456" }], + options: [ + { title: "size", values: ["large", "small"] }, + { title: "color", values: ["green"] }, + ], + variants: [ + { + title: "Test variant", + inventory_items: [ + { + inventory_item_id: inventoryItem.id, + required_quantity: 1, + }, + ], + prices: [ + { + currency_code: "usd", + amount: 100, + }, + ], + options: { + size: "large", + color: "green", + }, + }, + ], }, - [Modules.INVENTORY]: { - inventory_item_id: inventoryItem.id, - }, - }, - ]) + adminHeaders + ) + ).data.product shippingOption = ( await api.post( diff --git a/integration-tests/modules/__tests__/common/workflows.spec.ts b/integration-tests/modules/__tests__/common/workflows.spec.ts index dd8fd9bcb0..1ca048ae5b 100644 --- a/integration-tests/modules/__tests__/common/workflows.spec.ts +++ b/integration-tests/modules/__tests__/common/workflows.spec.ts @@ -96,7 +96,8 @@ medusaIntegrationTestRunner({ ) ).data.variant - expect(updatedVariant.inventory_items).toHaveLength(0) + // The default inventory item remains that was created as a part of create product + expect(updatedVariant.inventory_items).toHaveLength(1) }) }) }) @@ -159,11 +160,14 @@ medusaIntegrationTestRunner({ ) ).data.variant - expect(updatedVariant.inventory_items).toEqual([ - expect.objectContaining({ - required_quantity: originalQuantity, - }), - ]) + expect(updatedVariant.inventory_items).toHaveLength(2) + expect(updatedVariant.inventory_items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + required_quantity: originalQuantity, + }), + ]) + ) }) it("should throw an error when a link is not found", async () => { @@ -258,11 +262,14 @@ medusaIntegrationTestRunner({ ) ).data.variant - expect(updatedVariant.inventory_items).toEqual([ - expect.objectContaining({ - required_quantity: originalQuantity, - }), - ]) + expect(updatedVariant.inventory_items).toHaveLength(2) + expect(updatedVariant.inventory_items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + required_quantity: originalQuantity, + }), + ]) + ) }) it("should pass dismiss step if link not found if next step throws error", async () => { @@ -304,7 +311,7 @@ medusaIntegrationTestRunner({ ) ).data.variant - expect(updatedVariant.inventory_items).toEqual([]) + expect(updatedVariant.inventory_items).toHaveLength(1) }) }) }) diff --git a/integration-tests/modules/__tests__/product/store/index.spec.ts b/integration-tests/modules/__tests__/product/store/index.spec.ts index 5a9a533c50..b63fa1dbe3 100644 --- a/integration-tests/modules/__tests__/product/store/index.spec.ts +++ b/integration-tests/modules/__tests__/product/store/index.spec.ts @@ -27,6 +27,8 @@ medusaIntegrationTestRunner({ let variant2 let variant3 let variant4 + let inventoryItem1 + let inventoryItem2 const createProducts = async (data) => { const response = await api.post( @@ -88,6 +90,21 @@ medusaIntegrationTestRunner({ describe("GET /store/products", () => { beforeEach(async () => { + inventoryItem1 = ( + await api.post( + `/admin/inventory-items`, + { sku: "test-sku" }, + adminHeaders + ) + ).data.inventory_item + + inventoryItem2 = ( + await api.post( + `/admin/inventory-items`, + { sku: "test-sku-2" }, + adminHeaders + ) + ).data.inventory_item ;[product, [variant]] = await createProducts({ title: "test product 1", status: ProductStatus.PUBLISHED, @@ -95,6 +112,16 @@ medusaIntegrationTestRunner({ { title: "test variant 1", manage_inventory: true, + inventory_items: [ + { + inventory_item_id: inventoryItem1.id, + required_quantity: 20, + }, + { + inventory_item_id: inventoryItem2.id, + required_quantity: 20, + }, + ], prices: [{ amount: 3000, currency_code: "usd" }], }, ], @@ -368,8 +395,6 @@ medusaIntegrationTestRunner({ describe("with inventory items", () => { let location1 let location2 - let inventoryItem1 - let inventoryItem2 let salesChannel1 let publishableKey1 @@ -409,43 +434,23 @@ medusaIntegrationTestRunner({ adminHeaders ) - inventoryItem1 = ( - await api.post( - `/admin/inventory-items`, - { sku: "test-sku" }, - adminHeaders - ) - ).data.inventory_item + await api.post( + `/admin/inventory-items/${inventoryItem1.id}/location-levels`, + { + location_id: location1.id, + stocked_quantity: 20, + }, + adminHeaders + ) - inventoryItem2 = ( - await api.post( - `/admin/inventory-items`, - { sku: "test-sku-2" }, - adminHeaders - ) - ).data.inventory_item - - inventoryItem1 = ( - await api.post( - `/admin/inventory-items/${inventoryItem1.id}/location-levels`, - { - location_id: location1.id, - stocked_quantity: 20, - }, - adminHeaders - ) - ).data.inventory_item - - inventoryItem2 = ( - await api.post( - `/admin/inventory-items/${inventoryItem2.id}/location-levels`, - { - location_id: location2.id, - stocked_quantity: 30, - }, - adminHeaders - ) - ).data.inventory_item + await api.post( + `/admin/inventory-items/${inventoryItem2.id}/location-levels`, + { + location_id: location2.id, + stocked_quantity: 30, + }, + adminHeaders + ) const remoteLink = appContainer.resolve( ContainerRegistrationKeys.REMOTE_LINK @@ -465,24 +470,6 @@ medusaIntegrationTestRunner({ }) it("should list all inventory items for a variant", async () => { - const remoteLink = appContainer.resolve( - ContainerRegistrationKeys.REMOTE_LINK - ) - - // TODO: Missing API endpoint. Remove this when its available - await remoteLink.create([ - { - [Modules.PRODUCT]: { variant_id: variant.id }, - [Modules.INVENTORY]: { inventory_item_id: inventoryItem1.id }, - data: { required_quantity: 20 }, - }, - { - [Modules.PRODUCT]: { variant_id: variant.id }, - [Modules.INVENTORY]: { inventory_item_id: inventoryItem2.id }, - data: { required_quantity: 20 }, - }, - ]) - let response = await api.get( `/store/products?sales_channel_id[]=${salesChannel1.id}&fields=variants.inventory_items.inventory.location_levels.*`, { headers: { "x-publishable-api-key": publishableKey1.token } } @@ -512,29 +499,21 @@ medusaIntegrationTestRunner({ }) it("should return inventory quantity when variant's manage_inventory is true", async () => { - const remoteLink = appContainer.resolve( - ContainerRegistrationKeys.REMOTE_LINK + await api.post( + `/admin/products/${product.id}/variants/${variant.id}/inventory-items`, + { required_quantity: 20, inventory_item_id: inventoryItem1.id }, + adminHeaders ) - // TODO: Missing API endpoint. Remove this when its available - await remoteLink.create([ - { - [Modules.PRODUCT]: { variant_id: variant.id }, - [Modules.INVENTORY]: { inventory_item_id: inventoryItem1.id }, - data: { required_quantity: 20 }, - }, - { - [Modules.PRODUCT]: { variant_id: variant.id }, - [Modules.INVENTORY]: { inventory_item_id: inventoryItem2.id }, - data: { required_quantity: 20 }, - }, - ]) + await api.post( + `/admin/products/${product.id}/variants/${variant.id}/inventory-items`, + { required_quantity: 20, inventory_item_id: inventoryItem2.id }, + adminHeaders + ) let response = await api.get( `/store/products?sales_channel_id[]=${salesChannel1.id}&fields=%2bvariants.inventory_quantity`, - { - headers: { "x-publishable-api-key": publishableKey1.token }, - } + { headers: { "x-publishable-api-key": publishableKey1.token } } ) const product1Res = response.data.products.find( diff --git a/packages/core/core-flows/src/inventory/steps/validate-inventory-items.ts b/packages/core/core-flows/src/inventory/steps/validate-inventory-items.ts new file mode 100644 index 0000000000..3a434fef0e --- /dev/null +++ b/packages/core/core-flows/src/inventory/steps/validate-inventory-items.ts @@ -0,0 +1,36 @@ +import { + ContainerRegistrationKeys, + MedusaError, + arrayDifference, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { createStep } from "@medusajs/workflows-sdk" + +export const validateInventoryItemsId = "validate-inventory-items-step" +export const validateInventoryItems = createStep( + validateInventoryItemsId, + async (id: string[], { container }) => { + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + + const query = remoteQueryObjectFromString({ + entryPoint: "inventory_item", + variables: { id }, + fields: ["id"], + }) + + const items = await remoteQuery(query) + const diff = arrayDifference( + id, + items.map(({ id }) => id) + ) + + if (diff.length > 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Inventory Items with ids: ${diff.join(", ")} was not found` + ) + } + } +) diff --git a/packages/core/core-flows/src/product/workflows/create-product-variants.ts b/packages/core/core-flows/src/product/workflows/create-product-variants.ts index a3068a2451..f55b74028b 100644 --- a/packages/core/core-flows/src/product/workflows/create-product-variants.ts +++ b/packages/core/core-flows/src/product/workflows/create-product-variants.ts @@ -1,9 +1,13 @@ -import { PricingTypes, ProductTypes } from "@medusajs/types" +import { LinkDefinition, Modules } from "@medusajs/modules-sdk" +import { InventoryNext, PricingTypes, ProductTypes } from "@medusajs/types" import { WorkflowData, createWorkflow, transform, } from "@medusajs/workflows-sdk" +import { createLinksWorkflow } from "../../common/workflows/create-links" +import { validateInventoryItems } from "../../inventory/steps/validate-inventory-items" +import { createInventoryItemsWorkflow } from "../../inventory/workflows/create-inventory-items" import { createPriceSetsStep } from "../../pricing" import { createProductVariantsStep } from "../steps/create-product-variants" import { createVariantPricingLinkStep } from "../steps/create-variant-pricing-link" @@ -12,9 +16,105 @@ import { createVariantPricingLinkStep } from "../steps/create-variant-pricing-li type WorkflowInput = { product_variants: (ProductTypes.CreateProductVariantDTO & { prices?: PricingTypes.CreateMoneyAmountDTO[] + } & { + inventory_items?: { + inventory_item_id: string + required_quantity?: number + }[] })[] } +const buildLink = ( + variant_id: string, + inventory_item_id: string, + required_quantity: number +) => { + const link: LinkDefinition = { + [Modules.PRODUCT]: { variant_id }, + [Modules.INVENTORY]: { inventory_item_id: inventory_item_id }, + data: { required_quantity: required_quantity }, + } + + return link +} + +const buildLinksToCreate = (data: { + createdVariants: ProductTypes.ProductVariantDTO[] + inventoryIndexMap: Record + input: WorkflowInput +}) => { + let index = 0 + const linksToCreate: LinkDefinition[] = [] + + for (const variant of data.createdVariants) { + const variantInput = data.input.product_variants[index] + const shouldManageInventory = variant.manage_inventory + const hasInventoryItems = variantInput.inventory_items?.length + index += 1 + + if (!shouldManageInventory) { + continue + } + + if (!hasInventoryItems) { + const inventoryItem = data.inventoryIndexMap[index] + + linksToCreate.push(buildLink(variant.id, inventoryItem.id, 1)) + + continue + } + + for (const inventoryInput of variantInput.inventory_items || []) { + linksToCreate.push( + buildLink( + variant.id, + inventoryInput.inventory_item_id, + inventoryInput.required_quantity ?? 1 + ) + ) + } + } + + return linksToCreate +} + +const buildVariantItemCreateMap = (data: { + createdVariants: ProductTypes.ProductVariantDTO[] + input: WorkflowInput +}) => { + let index = 0 + const map: Record = {} + + for (const variant of data.createdVariants || []) { + const variantInput = data.input.product_variants[index] + const shouldManageInventory = variant.manage_inventory + const hasInventoryItems = variantInput.inventory_items?.length + index += 1 + + if (!shouldManageInventory || hasInventoryItems) { + continue + } + + // Create a default inventory item if the above conditions arent met + map[index] = { + sku: variantInput.sku, + origin_country: variantInput.origin_country, + mid_code: variantInput.mid_code, + material: variantInput.material, + weight: variantInput.weight, + length: variantInput.length, + height: variantInput.height, + width: variantInput.width, + title: variantInput.title, + description: variantInput.title, + hs_code: variantInput.hs_code, + requires_shipping: true, + } + } + + return map +} + export const createProductVariantsWorkflowId = "create-product-variants" export const createProductVariantsWorkflow = createWorkflow( createProductVariantsWorkflowId, @@ -31,6 +131,50 @@ export const createProductVariantsWorkflow = createWorkflow( const createdVariants = createProductVariantsStep(variantsWithoutPrices) + // Setup variants inventory + const inventoryItemIds = transform(input, (data) => { + return data.product_variants + .map((variant) => variant.inventory_items || []) + .flat() + .map((item) => item.inventory_item_id) + .flat() + }) + + validateInventoryItems(inventoryItemIds) + + const variantItemCreateMap = transform( + { createdVariants, input }, + buildVariantItemCreateMap + ) + + const createdInventoryItems = createInventoryItemsWorkflow.runAsStep({ + input: { + items: transform(variantItemCreateMap, (data) => Object.values(data)), + }, + }) + + const inventoryIndexMap = transform( + { createdInventoryItems, variantItemCreateMap }, + (data) => { + const map: Record = {} + let inventoryIndex = 0 + + for (const variantIndex of Object.keys(data.variantItemCreateMap)) { + map[variantIndex] = data.createdInventoryItems[inventoryIndex] + inventoryIndex += 1 + } + + return map + } + ) + + const linksToCreate = transform( + { createdVariants, inventoryIndexMap, input }, + buildLinksToCreate + ) + + createLinksWorkflow.runAsStep({ input: linksToCreate }) + // Note: We rely on the same order of input and output when creating variants here, make sure that assumption holds const pricesToCreate = transform({ input, createdVariants }, (data) => data.createdVariants.map((v, i) => { @@ -40,7 +184,6 @@ export const createProductVariantsWorkflow = createWorkflow( }) ) - // TODO: From here until the final transform the code is the same as when creating a product, we can probably refactor const createdPriceSets = createPriceSetsStep(pricesToCreate) const variantAndPriceSets = transform( diff --git a/packages/medusa/src/api/admin/products/route.ts b/packages/medusa/src/api/admin/products/route.ts index ea533156c0..2ef50b2ddb 100644 --- a/packages/medusa/src/api/admin/products/route.ts +++ b/packages/medusa/src/api/admin/products/route.ts @@ -59,5 +59,6 @@ export const POST = async ( req.scope, req.remoteQueryConfig.fields ) + res.status(200).json({ product: remapProductResponse(product) }) } diff --git a/packages/medusa/src/api/admin/products/validators.ts b/packages/medusa/src/api/admin/products/validators.ts index 80dfd28bda..493dfee69a 100644 --- a/packages/medusa/src/api/admin/products/validators.ts +++ b/packages/medusa/src/api/admin/products/validators.ts @@ -128,27 +128,37 @@ export const AdminCreateProductType = z.object({ export type AdminCreateProductVariantType = z.infer< typeof AdminCreateProductVariant > -export const AdminCreateProductVariant = z.object({ - title: z.string(), - sku: z.string().nullable().optional(), - ean: z.string().nullable().optional(), - upc: z.string().nullable().optional(), - barcode: z.string().nullable().optional(), - hs_code: z.string().nullable().optional(), - mid_code: z.string().nullable().optional(), - allow_backorder: z.boolean().optional().default(false), - manage_inventory: z.boolean().optional().default(true), - variant_rank: z.number().optional(), - weight: z.number().nullable().optional(), - length: z.number().nullable().optional(), - height: z.number().nullable().optional(), - width: z.number().nullable().optional(), - origin_country: z.string().nullable().optional(), - material: z.string().nullable().optional(), - metadata: z.record(z.unknown()).optional(), - prices: z.array(AdminCreateVariantPrice), - options: z.record(z.string()).optional(), -}) +export const AdminCreateProductVariant = z + .object({ + title: z.string(), + sku: z.string().nullable().optional(), + ean: z.string().nullable().optional(), + upc: z.string().nullable().optional(), + barcode: z.string().nullable().optional(), + hs_code: z.string().nullable().optional(), + mid_code: z.string().nullable().optional(), + allow_backorder: z.boolean().optional().default(false), + manage_inventory: z.boolean().optional().default(true), + variant_rank: z.number().optional(), + weight: z.number().nullable().optional(), + length: z.number().nullable().optional(), + height: z.number().nullable().optional(), + width: z.number().nullable().optional(), + origin_country: z.string().nullable().optional(), + material: z.string().nullable().optional(), + metadata: z.record(z.unknown()).optional(), + prices: z.array(AdminCreateVariantPrice), + options: z.record(z.string()).optional(), + inventory_items: z + .array( + z.object({ + inventory_item_id: z.string(), + required_quantity: z.number(), + }) + ) + .optional(), + }) + .strict() export type AdminUpdateProductVariantType = z.infer< typeof AdminUpdateProductVariant diff --git a/packages/medusa/src/api/store/products/helpers.ts b/packages/medusa/src/api/store/products/helpers.ts index d5ea5d789e..08dad71b1a 100644 --- a/packages/medusa/src/api/store/products/helpers.ts +++ b/packages/medusa/src/api/store/products/helpers.ts @@ -87,8 +87,8 @@ export const wrapVariantsWithInventoryQuantity = async ( for (const link of links) { const requiredQuantity = link.required_quantity - const availableQuantity = (link.inventory.location_levels || []).reduce( - (sum, level) => sum + level.available_quantity || 0, + const availableQuantity = (link.inventory?.location_levels || []).reduce( + (sum, level) => sum + (level?.available_quantity || 0), 0 )