From 9c957e1da04c1f86d1caa7de279993453b76bb63 Mon Sep 17 00:00:00 2001 From: Leonardo Benini Date: Thu, 2 Oct 2025 14:31:53 +0200 Subject: [PATCH] chore(core-flows): only allow published products in addToCartWorkflow (#13182) Closes #13163 I have a few questions about expected behaviour, since this currently breaks some tests: - Many tests use the productModule to create products, with default status == "draft", and use the addToCart workflow which now throws. Should I change all breaking tests to specify status == "published" whne creating the product? The alternative would be to check the status in the store API route before the workflow but 1. it would be an extra query and 2. the addToCart workflow is only used in the store currently, and even if it was to be used admin-side, it still doesn't make sense to add a draft product to cart - After this PR an unpublished product would give the same error as a variant that doesn't exist. While imho this is correct, the thrown error (for both) is "Items do not have a price" which doesn't make much sense(i believe the workflows goes through with an empty variants list and then errors at the price check point). Should I throw a different error when a variant doesn't exists/isn't published? --- > [!NOTE] > Enforces that only variants from published products can be added to carts, adds status fetching, refines errors, and updates tests to use ProductStatus.PUBLISHED. > > - **Core Flows**: > - addToCart: Validate variants exist and belong to `product.status = PUBLISHED`; throw clear `INVALID_DATA` when not found/unpublished. > - Data fetching: Include `product.status` in `cart` and `order` variant field selections. > - **Tests/Fixtures**: > - Update integration tests to set `status: ProductStatus.PUBLISHED` when creating products and import `ProductStatus` where needed. > - Add cases for unpublished products and non-existent variants producing the new error message. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ca72532e957964d2d8e6bcecbb0905054c677ded. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .changeset/clever-avocados-jog.md | 5 + .../http/__tests__/cart/store/cart.spec.ts | 13 ++ .../http/__tests__/claims/claims.spec.ts | 3 + .../draft-order/admin/draft-order.spec.ts | 4 +- .../__tests__/exchanges/exchanges.spec.ts | 4 + .../http/__tests__/fixtures/order.ts | 3 +- .../__tests__/order-edits/order-edits.spec.ts | 4 + .../http/__tests__/order/admin/order.spec.ts | 12 +- .../promotions/admin/promotions.spec.ts | 3 +- .../store/shipping-option-calculated.spec.ts | 2 + .../store/shipping-option.spec.ts | 3 + .../__tests__/cart/store/cart.completion.ts | 5 + .../cart/store/cart.workflows.spec.ts | 158 ++++++++++++++++++ .../__tests__/cart/store/carts.spec.ts | 3 + .../__tests__/order/draft-order.spec.ts | 6 + .../order/workflows/__fixtures__/index.ts | 2 + .../workflows/begin-order-exchange.spec.ts | 2 + .../core/core-flows/src/cart/utils/fields.ts | 1 + .../get-variants-and-items-with-prices.ts | 13 ++ .../core/core-flows/src/order/utils/fields.ts | 1 + 20 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 .changeset/clever-avocados-jog.md diff --git a/.changeset/clever-avocados-jog.md b/.changeset/clever-avocados-jog.md new file mode 100644 index 0000000000..bbcab8816c --- /dev/null +++ b/.changeset/clever-avocados-jog.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +chore(core-flows): only allow published products in addToCartWorkflow diff --git a/integration-tests/http/__tests__/cart/store/cart.spec.ts b/integration-tests/http/__tests__/cart/store/cart.spec.ts index d690546102..6ff7470ff8 100644 --- a/integration-tests/http/__tests__/cart/store/cart.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart.spec.ts @@ -2036,6 +2036,7 @@ medusaIntegrationTestRunner({ { title: "Product without inventory management", description: "test", + status: ProductStatus.PUBLISHED, options: [ { title: "Size", @@ -2229,6 +2230,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Test fixture ${shippingProfile.id}`, + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [ { title: "pack", values: ["1-pack", "2-pack", "3-pack"] }, @@ -2548,6 +2550,7 @@ medusaIntegrationTestRunner({ `/admin/products`, { title: "test product", + status: ProductStatus.PUBLISHED, description: "test", options: [ { @@ -2893,6 +2896,7 @@ medusaIntegrationTestRunner({ { title: "Gift Card", description: "test", + status: ProductStatus.PUBLISHED, is_giftcard: true, options: [ { @@ -3452,6 +3456,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Medusa T-Shirt not discountable", + status: ProductStatus.PUBLISHED, handle: "t-shirt-not-discountable", discountable: false, options: [ @@ -3628,6 +3633,7 @@ medusaIntegrationTestRunner({ { title: "Product for free", description: "test", + status: ProductStatus.PUBLISHED, options: [ { title: "Size", @@ -3744,6 +3750,7 @@ medusaIntegrationTestRunner({ { title: "Product for free", description: "test", + status: ProductStatus.PUBLISHED, options: [ { title: "Size", @@ -3862,6 +3869,7 @@ medusaIntegrationTestRunner({ { title: "Product for free", description: "test", + status: ProductStatus.PUBLISHED, options: [ { title: "Size", @@ -3980,6 +3988,7 @@ medusaIntegrationTestRunner({ { title: "Product for free", description: "test", + status: ProductStatus.PUBLISHED, options: [ { title: "Size", @@ -4117,6 +4126,7 @@ medusaIntegrationTestRunner({ { title: "Product for free", description: "test", + status: ProductStatus.PUBLISHED, options: [ { title: "Size", @@ -4233,6 +4243,7 @@ medusaIntegrationTestRunner({ { title: "Product for free", description: "test", + status: ProductStatus.PUBLISHED, options: [ { title: "Size", @@ -4370,6 +4381,7 @@ medusaIntegrationTestRunner({ { title: "Product for free", description: "test", + status: ProductStatus.PUBLISHED, options: [ { title: "Size", @@ -4464,6 +4476,7 @@ medusaIntegrationTestRunner({ `/admin/products`, { title: "Product for free", + status: ProductStatus.PUBLISHED, description: "test", options: [ { diff --git a/integration-tests/http/__tests__/claims/claims.spec.ts b/integration-tests/http/__tests__/claims/claims.spec.ts index 3246be0980..cce6bd0336 100644 --- a/integration-tests/http/__tests__/claims/claims.spec.ts +++ b/integration-tests/http/__tests__/claims/claims.spec.ts @@ -4,6 +4,7 @@ import { ClaimType, ContainerRegistrationKeys, Modules, + ProductStatus, RuleOperator, } from "@medusajs/utils" import { @@ -82,6 +83,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Test product", + status: ProductStatus.PUBLISHED, options: [{ title: "size", values: ["large", "small"] }], shipping_profile_id: shippingProfile.id, variants: [ @@ -107,6 +109,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Extra product", + status: ProductStatus.PUBLISHED, options: [{ title: "size", values: ["large", "small"] }], shipping_profile_id: shippingProfile.id, variants: [ diff --git a/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts b/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts index 535107a13e..178b0fed00 100644 --- a/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts +++ b/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts @@ -1,6 +1,6 @@ import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { HttpTypes } from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/utils" +import { ModuleRegistrationName, ProductStatus } from "@medusajs/utils" import { adminHeaders, createAdminUser, } from "../../../../helpers/create-admin-user" import { setupTaxStructure } from "../../../../modules/__tests__/fixtures" @@ -312,6 +312,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Shirt", + status: ProductStatus.PUBLISHED, options: [ { title: "size", values: ["large", "medium", "small"] }, ], @@ -537,6 +538,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Shirt", + status: ProductStatus.PUBLISHED, options: [{ title: "size", values: ["large", "small"] }], variants: [ { diff --git a/integration-tests/http/__tests__/exchanges/exchanges.spec.ts b/integration-tests/http/__tests__/exchanges/exchanges.spec.ts index b74a57fb94..b72de66c80 100644 --- a/integration-tests/http/__tests__/exchanges/exchanges.spec.ts +++ b/integration-tests/http/__tests__/exchanges/exchanges.spec.ts @@ -2,6 +2,7 @@ import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { ContainerRegistrationKeys, Modules, + ProductStatus, RuleOperator, } from "@medusajs/utils" import { @@ -80,6 +81,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Test product", + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [{ title: "size", values: ["large", "small"] }], variants: [ @@ -105,6 +107,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Extra product", + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [{ title: "size", values: ["large", "small"] }], variants: [ @@ -130,6 +133,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Extra product 2, same price", + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [{ title: "size", values: ["large", "small"] }], variants: [ diff --git a/integration-tests/http/__tests__/fixtures/order.ts b/integration-tests/http/__tests__/fixtures/order.ts index a8626cf96c..4f1f0bf61b 100644 --- a/integration-tests/http/__tests__/fixtures/order.ts +++ b/integration-tests/http/__tests__/fixtures/order.ts @@ -6,7 +6,7 @@ import { AdminStockLocation, MedusaContainer, } from "@medusajs/types" -import { ContainerRegistrationKeys, Modules } from "@medusajs/utils" +import { ContainerRegistrationKeys, Modules, ProductStatus } from "@medusajs/utils" import { adminHeaders, generatePublishableKey, @@ -116,6 +116,7 @@ export async function createOrderSeeder({ "/admin/products", { title: `Test fixture ${shippingProfile.id}`, + status: ProductStatus.PUBLISHED, shipping_profile_id: withoutShipping ? undefined : shippingProfile.id, options: [ { title: "size", values: ["large", "small"] }, diff --git a/integration-tests/http/__tests__/order-edits/order-edits.spec.ts b/integration-tests/http/__tests__/order-edits/order-edits.spec.ts index f2b1237f70..1c8fc8ee01 100644 --- a/integration-tests/http/__tests__/order-edits/order-edits.spec.ts +++ b/integration-tests/http/__tests__/order-edits/order-edits.spec.ts @@ -3,6 +3,7 @@ import { ContainerRegistrationKeys, Modules, OrderChangeStatus, + ProductStatus, RuleOperator, } from "@medusajs/utils" import { @@ -109,6 +110,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Test product", + status: ProductStatus.PUBLISHED, options: [{ title: "size", values: ["large", "small"] }], shipping_profile_id: shippingProfile.id, variants: [ @@ -134,6 +136,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Extra product", + status: ProductStatus.PUBLISHED, options: [{ title: "size", values: ["large", "small"] }], shipping_profile_id: shippingProfile.id, variants: [ @@ -607,6 +610,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Shirt", + status: ProductStatus.PUBLISHED, options: [ { title: "size", values: ["large", "medium", "small"] }, ], diff --git a/integration-tests/http/__tests__/order/admin/order.spec.ts b/integration-tests/http/__tests__/order/admin/order.spec.ts index cdb164410b..a863e84a5b 100644 --- a/integration-tests/http/__tests__/order/admin/order.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order.spec.ts @@ -1,6 +1,6 @@ import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { AdminShippingOption } from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/utils" +import { ModuleRegistrationName, ProductStatus } from "@medusajs/utils" import { adminHeaders, createAdminUser, @@ -705,6 +705,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Test fixture`, + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [ { title: "size", values: ["large", "small"] }, @@ -793,6 +794,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Test fixture 2`, + status: ProductStatus.PUBLISHED, options: [ { title: "size", values: ["large", "small"] }, { title: "color", values: ["green"] }, @@ -829,6 +831,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Test fixture 3`, + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [ { title: "size", values: ["large", "small"] }, @@ -883,6 +886,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Test override 4`, + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [{ title: "size", values: ["large"] }], variants: [ @@ -924,6 +928,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Test fixture 4`, + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfileOverride.id, options: [ { title: "size", values: ["large", "small"] }, @@ -1310,6 +1315,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Wooden table`, + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [{ title: "color", values: ["green"] }], variants: [ @@ -1474,6 +1480,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Tablet`, + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [{ title: "color", values: ["green"] }], variants: [ @@ -1697,6 +1704,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Tablet`, + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [{ title: "color", values: ["green"] }], variants: [ @@ -2101,6 +2109,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Bottle Packs`, + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [{ title: "packs", values: ["one", "two", "three"] }], variants: [ @@ -2907,6 +2916,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: `Wooden table`, + status: ProductStatus.PUBLISHED, shipping_profile_id: shippingProfile.id, options: [{ title: "color", values: ["green"] }], variants: [ diff --git a/integration-tests/http/__tests__/promotions/admin/promotions.spec.ts b/integration-tests/http/__tests__/promotions/admin/promotions.spec.ts index 58b941e0b0..6cd4bb1639 100644 --- a/integration-tests/http/__tests__/promotions/admin/promotions.spec.ts +++ b/integration-tests/http/__tests__/promotions/admin/promotions.spec.ts @@ -1,5 +1,5 @@ import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { Modules, PromotionStatus, PromotionType } from "@medusajs/utils" +import { Modules, ProductStatus, PromotionStatus, PromotionType } from "@medusajs/utils" import { createAdminUser, generatePublishableKey, @@ -1724,6 +1724,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Discounted Medusa T-Shirt", + status: ProductStatus.PUBLISHED, handle: "discounted-medusa-t-shirt", options: [ { diff --git a/integration-tests/http/__tests__/shipping-option/store/shipping-option-calculated.spec.ts b/integration-tests/http/__tests__/shipping-option/store/shipping-option-calculated.spec.ts index 61ffbc46f3..7ace8c0991 100644 --- a/integration-tests/http/__tests__/shipping-option/store/shipping-option-calculated.spec.ts +++ b/integration-tests/http/__tests__/shipping-option/store/shipping-option-calculated.spec.ts @@ -4,6 +4,7 @@ import { generatePublishableKey, generateStoreHeaders, } from "../../../../helpers/create-admin-user" +import { ProductStatus } from "@medusajs/utils" jest.setTimeout(50000) @@ -80,6 +81,7 @@ medusaIntegrationTestRunner({ { title: "Test fixture", shipping_profile_id: shippingProfile.id, + status: ProductStatus.PUBLISHED, options: [ { title: "size", values: ["large", "small"] }, { title: "color", values: ["green"] }, diff --git a/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts b/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts index 0fb0631483..2642474a6e 100644 --- a/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts +++ b/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts @@ -4,6 +4,7 @@ import { generatePublishableKey, generateStoreHeaders, } from "../../../../helpers/create-admin-user" +import { ProductStatus } from "@medusajs/utils" jest.setTimeout(50000) @@ -78,6 +79,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Test fixture", + status: ProductStatus.PUBLISHED, options: [ { title: "size", values: ["large", "small"] }, { title: "color", values: ["green"] }, @@ -634,6 +636,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Test prod", + status: ProductStatus.PUBLISHED, options: [ { title: "size", values: ["large", "small"] }, { title: "color", values: ["green"] }, diff --git a/integration-tests/modules/__tests__/cart/store/cart.completion.ts b/integration-tests/modules/__tests__/cart/store/cart.completion.ts index 33f1c67cb9..bfeb8d7c8c 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.completion.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.completion.ts @@ -26,6 +26,7 @@ import { import { ContainerRegistrationKeys, Modules, + ProductStatus, remoteQueryObjectFromString, } from "@medusajs/utils" import { @@ -316,6 +317,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -472,6 +474,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -605,6 +608,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -764,6 +768,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", 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 7eeb408a19..68b0eee107 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -36,6 +36,7 @@ import { Modules, PriceListStatus, PriceListType, + ProductStatus, RuleOperator, } from "@medusajs/utils" import { @@ -184,6 +185,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -422,6 +424,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -538,6 +541,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -687,6 +691,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -855,6 +860,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -1263,6 +1269,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -1456,6 +1463,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -1691,6 +1699,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -1809,6 +1818,153 @@ medusaIntegrationTestRunner({ ) }) + it("should throw if product is not published", async () => { + const salesChannel = await scModuleService.createSalesChannels({ + name: "Webshop", + }) + + const location = await stockLocationModule.createStockLocations({ + name: "Warehouse", + }) + + let cart = await cartModuleService.createCarts({ + currency_code: "usd", + sales_channel_id: salesChannel.id, + }) + + const [product] = await productModule.createProducts([ + { + title: "Test product", + status: ProductStatus.DRAFT, + variants: [ + { + title: "Test variant", + }, + ], + }, + ]) + + const inventoryItem = await inventoryModule.createInventoryItems({ + 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.createPriceSets({ + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + }) + + await pricingModule.createPricePreferences({ + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, + }) + + 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, + }, + }, + ]) + + cart = await cartModuleService.retrieveCart(cart.id, { + select: ["id", "region_id", "currency_code", "sales_channel_id"], + }) + + const { errors } = await addToCartWorkflow(appContainer).run({ + input: { + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + cart_id: cart.id, + }, + throwOnError: false, + }) + + expect(errors).toEqual([ + { + action: "get-variant-items-with-prices-workflow-as-step", + handlerType: "invoke", + error: expect.objectContaining({ + message: expect.stringContaining( + `Variants ${product.variants[0].id} do not exist or belong to a product that is not published` + ), + }), + }, + ]) + }) + + it("should throw if variant doesn't exist", async () => { + const salesChannel = await scModuleService.createSalesChannels({ + name: "Webshop", + }) + + let cart = await cartModuleService.createCarts({ + currency_code: "usd", + sales_channel_id: salesChannel.id, + }) + + const { errors } = await addToCartWorkflow(appContainer).run({ + input: { + items: [ + { + variant_id: "var_1234", + quantity: 1, + }, + ], + cart_id: cart.id, + }, + throwOnError: false, + }) + + expect(errors).toEqual([ + { + action: "get-variant-items-with-prices-workflow-as-step", + handlerType: "invoke", + error: expect.objectContaining({ + message: expect.stringContaining( + `Variants var_1234 do not exist or belong to a product that is not published` + ), + }), + }, + ]) + }) + it("should throw if no price sets for variant exist", async () => { const salesChannel = await scModuleService.createSalesChannels({ name: "Webshop", @@ -1910,6 +2066,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -2062,6 +2219,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", diff --git a/integration-tests/modules/__tests__/cart/store/carts.spec.ts b/integration-tests/modules/__tests__/cart/store/carts.spec.ts index 98434e0685..110dfbc568 100644 --- a/integration-tests/modules/__tests__/cart/store/carts.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/carts.spec.ts @@ -109,6 +109,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { manage_inventory: false, @@ -215,6 +216,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product default tax", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant default tax", manage_inventory: false }, ], @@ -1334,6 +1336,7 @@ medusaIntegrationTestRunner({ "/admin/products", { title: "Test fixture", + status: ProductStatus.PUBLISHED, options: [ { title: "size", values: ["large", "small"] }, { title: "color", values: ["green"] }, diff --git a/integration-tests/modules/__tests__/order/draft-order.spec.ts b/integration-tests/modules/__tests__/order/draft-order.spec.ts index 553b312dc6..c5807cde77 100644 --- a/integration-tests/modules/__tests__/order/draft-order.spec.ts +++ b/integration-tests/modules/__tests__/order/draft-order.spec.ts @@ -11,6 +11,7 @@ import { import { ContainerRegistrationKeys, Modules, + ProductStatus, PromotionStatus, PromotionType, } from "@medusajs/utils" @@ -71,6 +72,7 @@ medusaIntegrationTestRunner({ const [product, product_2] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -79,6 +81,7 @@ medusaIntegrationTestRunner({ }, { title: "Another product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Variant variable", @@ -401,6 +404,7 @@ medusaIntegrationTestRunner({ const [product, product_2] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", @@ -409,6 +413,7 @@ medusaIntegrationTestRunner({ }, { title: "Another product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Variant variable", @@ -815,6 +820,7 @@ medusaIntegrationTestRunner({ const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, type_id: productType.id, variants: [ { diff --git a/integration-tests/modules/__tests__/order/workflows/__fixtures__/index.ts b/integration-tests/modules/__tests__/order/workflows/__fixtures__/index.ts index 11d6c8a783..05dece6752 100644 --- a/integration-tests/modules/__tests__/order/workflows/__fixtures__/index.ts +++ b/integration-tests/modules/__tests__/order/workflows/__fixtures__/index.ts @@ -9,6 +9,7 @@ import { import { ContainerRegistrationKeys, Modules, + ProductStatus, remoteQueryObjectFromString, } from "@medusajs/utils" @@ -94,6 +95,7 @@ export async function prepareDataFixtures({ container }) { const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", diff --git a/integration-tests/modules/__tests__/order/workflows/begin-order-exchange.spec.ts b/integration-tests/modules/__tests__/order/workflows/begin-order-exchange.spec.ts index e4568afa40..fe46960752 100644 --- a/integration-tests/modules/__tests__/order/workflows/begin-order-exchange.spec.ts +++ b/integration-tests/modules/__tests__/order/workflows/begin-order-exchange.spec.ts @@ -19,6 +19,7 @@ import { import { ContainerRegistrationKeys, Modules, + ProductStatus, RuleOperator, remoteQueryObjectFromString, } from "@medusajs/utils" @@ -108,6 +109,7 @@ async function prepareDataFixtures({ container }) { const [product] = await productModule.createProducts([ { title: "Test product", + status: ProductStatus.PUBLISHED, variants: [ { title: "Test variant", diff --git a/packages/core/core-flows/src/cart/utils/fields.ts b/packages/core/core-flows/src/cart/utils/fields.ts index 14cc80c0cb..21e3524781 100644 --- a/packages/core/core-flows/src/cart/utils/fields.ts +++ b/packages/core/core-flows/src/cart/utils/fields.ts @@ -157,6 +157,7 @@ export const productVariantsFields = [ "product.id", "product.title", "product.description", + "product.status", "product.subtitle", "product.thumbnail", "product.type.value", diff --git a/packages/core/core-flows/src/cart/workflows/get-variants-and-items-with-prices.ts b/packages/core/core-flows/src/cart/workflows/get-variants-and-items-with-prices.ts index df6ecfdef0..f82931cc03 100644 --- a/packages/core/core-flows/src/cart/workflows/get-variants-and-items-with-prices.ts +++ b/packages/core/core-flows/src/cart/workflows/get-variants-and-items-with-prices.ts @@ -13,6 +13,7 @@ import { filterObjectByKeys, isDefined, MedusaError, + ProductStatus, simpleHash, } from "@medusajs/framework/utils" import { @@ -167,6 +168,7 @@ export const getVariantsAndItemsWithPrices = createWorkflow( calculatedPriceSets, }): GetVariantsAndItemsWithPricesWorkflowOutput => { const priceNotFound: string[] = [] + const variantNotFoundOrPublished: string[] = [] const items = (inputItems ?? cart.items ?? []).map((item) => { const item_ = item as any @@ -182,6 +184,11 @@ export const getVariantsAndItemsWithPrices = createWorkflow( } const variant = variantsData.find((v) => v.id === item.variant_id) + if ((item.variant_id && !variant) || // variant specified but doesn't exist + (variant && (!variant?.product?.status || variant.product.status !== ProductStatus.PUBLISHED)) // variant exists but product is not published + ) { + variantNotFoundOrPublished.push(item_.variant_id) + } if (variant) { variant.calculated_price = calculatedPriceSet @@ -215,6 +222,12 @@ export const getVariantsAndItemsWithPrices = createWorkflow( } }) + if (variantNotFoundOrPublished.length > 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Variants ${variantNotFoundOrPublished.join(", ")} do not exist or belong to a product that is not published` + ) + } if (priceNotFound.length > 0) { throw new MedusaError( MedusaError.Types.INVALID_DATA, diff --git a/packages/core/core-flows/src/order/utils/fields.ts b/packages/core/core-flows/src/order/utils/fields.ts index e82712d9fa..59831a146f 100644 --- a/packages/core/core-flows/src/order/utils/fields.ts +++ b/packages/core/core-flows/src/order/utils/fields.ts @@ -8,6 +8,7 @@ export const productVariantsFields = [ "product.id", "product.title", "product.description", + "product.status", "product.subtitle", "product.thumbnail", "product.type.value",