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. > > <sup>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).</sup>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"] },
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user