fix: Ensure sales channel updates don't remove sales channel on other products (#7510)

* fix: Make all product tests pass

* fix: Ensure product update doesnt remove sales channels on other products
This commit is contained in:
Stevche Radevski authored and GitHub committed 2024-05-28 18:24:27 +02:00
1 parent dbb23d20be
commit af7a885b5b
6 files changed
+188 -52

No files matched your search

@@ -45,7 +45,6 @@ const getProductFixture = (overrides) => ({
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
inventory_quantity: 10,
prices: [ prices: [
{ {
currency_code: "usd", currency_code: "usd",
@@ -1075,13 +1074,15 @@ medusaIntegrationTestRunner({
return [baseProduct.id, salesChannel.id] return [baseProduct.id, salesChannel.id]
}, },
async () => { async () => {
const salesChannel = await simpleSalesChannelFactory( const salesChannel = (
dbConnection, await api.post(
"/admin/sales-channels",
{ {
name: "test name", name: "Sales",
description: "test description", },
} adminHeaders
) )
).data.sales_channel
// Currently the product update doesn't support managing sales channels // Currently the product update doesn't support managing sales channels
const newProduct = ( const newProduct = (
@@ -1423,7 +1424,6 @@ medusaIntegrationTestRunner({
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }], prices: [{ currency_code: "usd", amount: 100 }],
}, },
], ],
@@ -1446,6 +1446,7 @@ medusaIntegrationTestRunner({
it("Sets variant ranks when creating a product", async () => { it("Sets variant ranks when creating a product", async () => {
const payload = { const payload = {
title: "Test product - 1", title: "Test product - 1",
handle: "test-1",
description: "test-product-description 1", description: "test-product-description 1",
images: breaking( images: breaking(
() => ["test-image.png", "test-image-2.png"], () => ["test-image.png", "test-image-2.png"],
@@ -1456,12 +1457,10 @@ medusaIntegrationTestRunner({
variants: [ variants: [
{ {
title: "Test variant 1", title: "Test variant 1",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }], prices: [{ currency_code: "usd", amount: 100 }],
}, },
{ {
title: "Test variant 2", title: "Test variant 2",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }], prices: [{ currency_code: "usd", amount: 100 }],
}, },
], ],
@@ -1632,7 +1631,6 @@ medusaIntegrationTestRunner({
upc: "test-upc", upc: "test-upc",
created_at: expect.any(String), created_at: expect.any(String),
id: baseProduct.variants[0].id, id: baseProduct.variants[0].id,
inventory_quantity: 10,
manage_inventory: true, manage_inventory: true,
options: breaking( options: breaking(
() => () =>
@@ -1866,6 +1864,83 @@ medusaIntegrationTestRunner({
) )
}) })
it("updates multiple products that have the same sales channel", async () => {
const salesChannel = (
await api.post(
"/admin/sales-channels",
{
name: "Sales",
},
adminHeaders
)
).data.sales_channel
await api.post(
`/admin/products/${baseProduct.id}`,
{
sales_channels: [{ id: salesChannel.id }],
},
adminHeaders
)
await api.post(
`/admin/products/${proposedProduct.id}`,
{
sales_channels: [{ id: salesChannel.id }],
},
adminHeaders
)
let res = await api.get(
`/admin/products?fields=*sales_channels&sales_channel_id[]=${salesChannel.id}`,
adminHeaders
)
expect(res.status).toEqual(200)
expect(res.data.products).toEqual([
expect.objectContaining({
id: baseProduct.id,
sales_channels: expect.arrayContaining([
expect.objectContaining({
id: salesChannel.id,
}),
]),
}),
expect.objectContaining({
id: proposedProduct.id,
sales_channels: expect.arrayContaining([
expect.objectContaining({
id: salesChannel.id,
}),
]),
}),
])
await api.post(
`/admin/products/${proposedProduct.id}`,
{
sales_channels: [],
},
adminHeaders
)
res = await api.get(
`/admin/products?fields=*sales_channels&sales_channel_id[]=${salesChannel.id}`,
adminHeaders
)
expect(res.status).toEqual(200)
expect(res.data.products).toEqual([
expect.objectContaining({
id: baseProduct.id,
sales_channels: expect.arrayContaining([
expect.objectContaining({
id: salesChannel.id,
}),
]),
}),
])
})
it("fails to update product with invalid status", async () => { it("fails to update product with invalid status", async () => {
const payload = { const payload = {
status: null, status: null,
@@ -1896,15 +1971,12 @@ medusaIntegrationTestRunner({
variants: [ variants: [
{ {
title: "first", title: "first",
inventory_quantity: 10,
}, },
{ {
title: "second", title: "second",
inventory_quantity: 10,
}, },
{ {
title: "third", title: "third",
inventory_quantity: 10,
}, },
], ],
} }
@@ -2528,7 +2600,6 @@ medusaIntegrationTestRunner({
ean: "new-ean", ean: "new-ean",
upc: "new-upc", upc: "new-upc",
barcode: "new-barcode", barcode: "new-barcode",
inventory_quantity: 10,
prices: [ prices: [
{ {
currency_code: "usd", currency_code: "usd",
@@ -2790,7 +2861,6 @@ medusaIntegrationTestRunner({
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }], prices: [{ currency_code: "usd", amount: 100 }],
}, },
], ],
@@ -2821,7 +2891,6 @@ medusaIntegrationTestRunner({
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }], prices: [{ currency_code: "usd", amount: 100 }],
}, },
], ],
@@ -2833,7 +2902,7 @@ medusaIntegrationTestRunner({
expect(error.response.data.message).toMatch( expect(error.response.data.message).toMatch(
breaking( breaking(
() => "Product with handle base-product already exists.", () => "Product with handle base-product already exists.",
() => "Product with handle: base-product already exists." () => "Product with handle: base-product, already exists."
) )
) )
} }
@@ -2890,7 +2959,7 @@ medusaIntegrationTestRunner({
() => () =>
`Product_collection with handle ${baseCollection.handle} already exists.`, `Product_collection with handle ${baseCollection.handle} already exists.`,
() => () =>
`Product collection with handle: ${baseCollection.handle} already exists.` `Product collection with handle: ${baseCollection.handle}, already exists.`
) )
) )
} }
@@ -3059,7 +3128,6 @@ medusaIntegrationTestRunner({
variants: [ variants: [
{ {
title: "Variant 1", title: "Variant 1",
inventory_quantity: 5,
prices: [ prices: [
{ {
currency_code: "usd", currency_code: "usd",
@@ -3069,7 +3137,6 @@ medusaIntegrationTestRunner({
}, },
{ {
title: "Variant 2", title: "Variant 2",
inventory_quantity: 20,
prices: [ prices: [
{ {
currency_code: "usd", currency_code: "usd",
@@ -3090,7 +3157,6 @@ medusaIntegrationTestRunner({
const createPayload = { const createPayload = {
title: "Test batch create variant", title: "Test batch create variant",
inventory_quantity: 10,
prices: [ prices: [
{ {
currency_code: "usd", currency_code: "usd",
+56 -19
View File
@@ -5,29 +5,40 @@ const DB_USERNAME = process.env.DB_USERNAME
const DB_PASSWORD = process.env.DB_PASSWORD const DB_PASSWORD = process.env.DB_PASSWORD
const DB_NAME = process.env.DB_TEMP_NAME const DB_NAME = process.env.DB_TEMP_NAME
const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}` const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`
const redisUrl = process.env.REDIS_URL
const cacheTTL = process.env.CACHE_TTL ?? 15
const enableResponseCompression =
process.env.ENABLE_RESPONSE_COMPRESSION || true
const enableMedusaV2 = process.env.MEDUSA_FF_MEDUSA_V2 == "true"
process.env.POSTGRES_URL = DB_URL process.env.POSTGRES_URL = DB_URL
process.env.LOG_LEVEL = "error" process.env.LOG_LEVEL = "error"
const enableMedusaV2 = process.env.MEDUSA_FF_MEDUSA_V2 == "true"
const customPaymentProvider = {
resolve: {
services: [require("@medusajs/payment/dist/providers/system").default],
},
options: {
config: {
default_2: {},
},
},
}
const customFulfillmentProvider = {
resolve: "@medusajs/fulfillment-manual",
options: {
config: {
"test-provider": {},
},
},
}
module.exports = { module.exports = {
plugins: [],
admin: { admin: {
disable: true, disable: true,
}, },
plugins: [],
projectConfig: { projectConfig: {
redisUrl: redisUrl,
databaseUrl: DB_URL, databaseUrl: DB_URL,
databaseType: "postgres", databaseType: "postgres",
http: { http: {
compression: {
enabled: enableResponseCompression,
},
jwtSecret: "test", jwtSecret: "test",
cookieSecret: "test", cookieSecret: "test",
}, },
@@ -36,11 +47,6 @@ module.exports = {
medusa_v2: enableMedusaV2, medusa_v2: enableMedusaV2,
}, },
modules: { modules: {
cacheService: {
resolve: "@medusajs/cache-inmemory",
options: { ttl: cacheTTL },
},
workflows: true,
[Modules.AUTH]: true, [Modules.AUTH]: true,
[Modules.USER]: { [Modules.USER]: {
scope: "internal", scope: "internal",
@@ -80,6 +86,7 @@ module.exports = {
[Modules.PRODUCT]: true, [Modules.PRODUCT]: true,
[Modules.PRICING]: true, [Modules.PRICING]: true,
[Modules.PROMOTION]: true, [Modules.PROMOTION]: true,
[Modules.REGION]: true,
[Modules.CUSTOMER]: true, [Modules.CUSTOMER]: true,
[Modules.SALES_CHANNEL]: true, [Modules.SALES_CHANNEL]: true,
[Modules.CART]: true, [Modules.CART]: true,
@@ -89,7 +96,37 @@ module.exports = {
[Modules.STORE]: true, [Modules.STORE]: true,
[Modules.TAX]: true, [Modules.TAX]: true,
[Modules.CURRENCY]: true, [Modules.CURRENCY]: true,
[Modules.PAYMENT]: true, [Modules.ORDER]: true,
[Modules.FULFILLMENT]: true, [Modules.PAYMENT]: {
resolve: "@medusajs/payment",
/** @type {import('@medusajs/payment').PaymentModuleOptions}*/
options: {
providers: [customPaymentProvider],
},
},
[Modules.FULFILLMENT]: {
/** @type {import('@medusajs/fulfillment').FulfillmentModuleOptions} */
options: {
providers: [customFulfillmentProvider],
},
},
[Modules.NOTIFICATION]: {
/** @type {import('@medusajs/types').LocalNotificationServiceOptions} */
options: {
providers: [
{
resolve: "@medusajs/notification-local",
options: {
config: {
"local-notification-provider": {
name: "Local Notification Provider",
channels: ["log", "email"],
},
},
},
},
],
},
},
}, },
} }
@@ -1,3 +1,4 @@
export * from "./steps/remove-remote-links" export * from "./steps/remove-remote-links"
export * from "./steps/use-remote-query" export * from "./steps/use-remote-query"
export * from "./steps/create-remote-links" export * from "./steps/create-remote-links"
export * from "./steps/dismiss-remote-links"
@@ -0,0 +1,30 @@
import { LinkDefinition, RemoteLink } from "@medusajs/modules-sdk"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
import { ContainerRegistrationKeys } from "@medusajs/utils"
type DismissRemoteLinksStepInput = LinkDefinition | LinkDefinition[]
export const dismissRemoteLinkStepId = "dismiss-remote-links"
export const dismissRemoteLinkStep = createStep(
dismissRemoteLinkStepId,
async (data: DismissRemoteLinksStepInput, { container }) => {
const entries = Array.isArray(data) ? data : [data]
const link = container.resolve<RemoteLink>(
ContainerRegistrationKeys.REMOTE_LINK
)
await link.dismiss(entries)
return new StepResponse(entries, entries)
},
async (dismissdLinks, { container }) => {
if (!dismissdLinks) {
return
}
const link = container.resolve<RemoteLink>(
ContainerRegistrationKeys.REMOTE_LINK
)
await link.create(dismissdLinks)
}
)
@@ -1,18 +1,18 @@
import { ProductTypes } from "@medusajs/types"
import {
createWorkflow,
transform,
WorkflowData,
} from "@medusajs/workflows-sdk"
import { updateProductsStep } from "../steps/update-products" import { updateProductsStep } from "../steps/update-products"
import { import {
dismissRemoteLinkStep,
createLinkStep, createLinkStep,
removeRemoteLinkStep,
useRemoteQueryStep, useRemoteQueryStep,
} from "../../common" } from "../../common"
import { arrayDifference } from "@medusajs/utils" import { arrayDifference } from "@medusajs/utils"
import { DeleteEntityInput, Modules } from "@medusajs/modules-sdk" import { Modules } from "@medusajs/modules-sdk"
import { ProductTypes } from "@medusajs/types"
import {
WorkflowData,
createWorkflow,
transform,
} from "@medusajs/workflows-sdk"
type UpdateProductsStepInputSelector = { type UpdateProductsStepInputSelector = {
selector: ProductTypes.FilterableProductProps selector: ProductTypes.FilterableProductProps
@@ -63,15 +63,16 @@ function updateProductIds({
updatedProducts: ProductTypes.ProductDTO[] updatedProducts: ProductTypes.ProductDTO[]
input: WorkflowInput input: WorkflowInput
}) { }) {
if ("products" in input) {
let productIds = updatedProducts.map((p) => p.id) let productIds = updatedProducts.map((p) => p.id)
if ("products" in input) {
const discardedProductIds: string[] = input.products const discardedProductIds: string[] = input.products
.filter((p) => !p.sales_channels) .filter((p) => !p.sales_channels)
.map((p) => p.id as string) .map((p) => p.id as string)
return arrayDifference(productIds, discardedProductIds) return arrayDifference(productIds, discardedProductIds)
} }
return !input.update.sales_channels ? [] : undefined return !input.update.sales_channels ? [] : productIds
} }
function prepareSalesChannelLinks({ function prepareSalesChannelLinks({
@@ -148,12 +149,12 @@ export const updateProductsWorkflow = createWorkflow(
const currentLinks = useRemoteQueryStep({ const currentLinks = useRemoteQueryStep({
entry_point: "product_sales_channel", entry_point: "product_sales_channel",
fields: ["product_id", "sales_channel_id"], fields: ["product_id", "sales_channel_id"],
variables: { product_id: updatedProductIds }, variables: { filters: { product_id: updatedProductIds } },
}) })
const toDeleteLinks = transform({ currentLinks }, prepareToDeleteLinks) const toDeleteLinks = transform({ currentLinks }, prepareToDeleteLinks)
removeRemoteLinkStep(toDeleteLinks as DeleteEntityInput[]) dismissRemoteLinkStep(toDeleteLinks)
const salesChannelLinks = transform( const salesChannelLinks = transform(
{ input, updatedProducts }, { input, updatedProducts },
@@ -42,6 +42,7 @@ export const AdminGetProductsParams = createFindParams({
.object({ .object({
variants: AdminGetProductVariantsParams.optional(), variants: AdminGetProductVariantsParams.optional(),
price_list_id: z.string().array().optional(), price_list_id: z.string().array().optional(),
status: statusEnum.array().optional(),
$and: z.lazy(() => AdminGetProductsParams.array()).optional(), $and: z.lazy(() => AdminGetProductsParams.array()).optional(),
$or: z.lazy(() => AdminGetProductsParams.array()).optional(), $or: z.lazy(() => AdminGetProductsParams.array()).optional(),
}) })