From af7a885b5bc23744f64ba424fbef2b3516db4f5b Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Tue, 28 May 2024 18:24:27 +0200 Subject: [PATCH] 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 --- .../api/__tests__/admin/product.js | 112 ++++++++++++++---- integration-tests/api/medusa-config.js | 75 +++++++++--- packages/core/core-flows/src/common/index.ts | 1 + .../src/common/steps/dismiss-remote-links.ts | 30 +++++ .../src/product/workflows/update-products.ts | 25 ++-- .../src/api/admin/products/validators.ts | 1 + 6 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 packages/core/core-flows/src/common/steps/dismiss-remote-links.ts diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 259415fb20..6c177d7b1f 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -45,7 +45,6 @@ const getProductFixture = (overrides) => ({ variants: [ { title: "Test variant", - inventory_quantity: 10, prices: [ { currency_code: "usd", @@ -1075,13 +1074,15 @@ medusaIntegrationTestRunner({ return [baseProduct.id, salesChannel.id] }, async () => { - const salesChannel = await simpleSalesChannelFactory( - dbConnection, - { - name: "test name", - description: "test description", - } - ) + const salesChannel = ( + await api.post( + "/admin/sales-channels", + { + name: "Sales", + }, + adminHeaders + ) + ).data.sales_channel // Currently the product update doesn't support managing sales channels const newProduct = ( @@ -1423,7 +1424,6 @@ medusaIntegrationTestRunner({ variants: [ { title: "Test variant", - inventory_quantity: 10, prices: [{ currency_code: "usd", amount: 100 }], }, ], @@ -1446,6 +1446,7 @@ medusaIntegrationTestRunner({ it("Sets variant ranks when creating a product", async () => { const payload = { title: "Test product - 1", + handle: "test-1", description: "test-product-description 1", images: breaking( () => ["test-image.png", "test-image-2.png"], @@ -1456,12 +1457,10 @@ medusaIntegrationTestRunner({ variants: [ { title: "Test variant 1", - inventory_quantity: 10, prices: [{ currency_code: "usd", amount: 100 }], }, { title: "Test variant 2", - inventory_quantity: 10, prices: [{ currency_code: "usd", amount: 100 }], }, ], @@ -1632,7 +1631,6 @@ medusaIntegrationTestRunner({ upc: "test-upc", created_at: expect.any(String), id: baseProduct.variants[0].id, - inventory_quantity: 10, manage_inventory: true, 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 () => { const payload = { status: null, @@ -1896,15 +1971,12 @@ medusaIntegrationTestRunner({ variants: [ { title: "first", - inventory_quantity: 10, }, { title: "second", - inventory_quantity: 10, }, { title: "third", - inventory_quantity: 10, }, ], } @@ -2528,7 +2600,6 @@ medusaIntegrationTestRunner({ ean: "new-ean", upc: "new-upc", barcode: "new-barcode", - inventory_quantity: 10, prices: [ { currency_code: "usd", @@ -2790,7 +2861,6 @@ medusaIntegrationTestRunner({ variants: [ { title: "Test variant", - inventory_quantity: 10, prices: [{ currency_code: "usd", amount: 100 }], }, ], @@ -2821,7 +2891,6 @@ medusaIntegrationTestRunner({ variants: [ { title: "Test variant", - inventory_quantity: 10, prices: [{ currency_code: "usd", amount: 100 }], }, ], @@ -2833,7 +2902,7 @@ medusaIntegrationTestRunner({ expect(error.response.data.message).toMatch( breaking( () => "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.` ) ) } @@ -3059,7 +3128,6 @@ medusaIntegrationTestRunner({ variants: [ { title: "Variant 1", - inventory_quantity: 5, prices: [ { currency_code: "usd", @@ -3069,7 +3137,6 @@ medusaIntegrationTestRunner({ }, { title: "Variant 2", - inventory_quantity: 20, prices: [ { currency_code: "usd", @@ -3090,7 +3157,6 @@ medusaIntegrationTestRunner({ const createPayload = { title: "Test batch create variant", - inventory_quantity: 10, prices: [ { currency_code: "usd", diff --git a/integration-tests/api/medusa-config.js b/integration-tests/api/medusa-config.js index ec21558b0c..1de336b775 100644 --- a/integration-tests/api/medusa-config.js +++ b/integration-tests/api/medusa-config.js @@ -5,29 +5,40 @@ const DB_USERNAME = process.env.DB_USERNAME const DB_PASSWORD = process.env.DB_PASSWORD const DB_NAME = process.env.DB_TEMP_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.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 = { - plugins: [], admin: { disable: true, }, + plugins: [], projectConfig: { - redisUrl: redisUrl, databaseUrl: DB_URL, databaseType: "postgres", http: { - compression: { - enabled: enableResponseCompression, - }, jwtSecret: "test", cookieSecret: "test", }, @@ -36,11 +47,6 @@ module.exports = { medusa_v2: enableMedusaV2, }, modules: { - cacheService: { - resolve: "@medusajs/cache-inmemory", - options: { ttl: cacheTTL }, - }, - workflows: true, [Modules.AUTH]: true, [Modules.USER]: { scope: "internal", @@ -80,6 +86,7 @@ module.exports = { [Modules.PRODUCT]: true, [Modules.PRICING]: true, [Modules.PROMOTION]: true, + [Modules.REGION]: true, [Modules.CUSTOMER]: true, [Modules.SALES_CHANNEL]: true, [Modules.CART]: true, @@ -89,7 +96,37 @@ module.exports = { [Modules.STORE]: true, [Modules.TAX]: true, [Modules.CURRENCY]: true, - [Modules.PAYMENT]: true, - [Modules.FULFILLMENT]: true, + [Modules.ORDER]: 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"], + }, + }, + }, + }, + ], + }, + }, }, } diff --git a/packages/core/core-flows/src/common/index.ts b/packages/core/core-flows/src/common/index.ts index 013c43af22..08541ae8c3 100644 --- a/packages/core/core-flows/src/common/index.ts +++ b/packages/core/core-flows/src/common/index.ts @@ -1,3 +1,4 @@ export * from "./steps/remove-remote-links" export * from "./steps/use-remote-query" export * from "./steps/create-remote-links" +export * from "./steps/dismiss-remote-links" diff --git a/packages/core/core-flows/src/common/steps/dismiss-remote-links.ts b/packages/core/core-flows/src/common/steps/dismiss-remote-links.ts new file mode 100644 index 0000000000..8f5ccfd587 --- /dev/null +++ b/packages/core/core-flows/src/common/steps/dismiss-remote-links.ts @@ -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( + ContainerRegistrationKeys.REMOTE_LINK + ) + await link.dismiss(entries) + + return new StepResponse(entries, entries) + }, + async (dismissdLinks, { container }) => { + if (!dismissdLinks) { + return + } + + const link = container.resolve( + ContainerRegistrationKeys.REMOTE_LINK + ) + await link.create(dismissdLinks) + } +) diff --git a/packages/core/core-flows/src/product/workflows/update-products.ts b/packages/core/core-flows/src/product/workflows/update-products.ts index b4d6456f75..08966db385 100644 --- a/packages/core/core-flows/src/product/workflows/update-products.ts +++ b/packages/core/core-flows/src/product/workflows/update-products.ts @@ -1,18 +1,18 @@ -import { ProductTypes } from "@medusajs/types" -import { - createWorkflow, - transform, - WorkflowData, -} from "@medusajs/workflows-sdk" import { updateProductsStep } from "../steps/update-products" import { + dismissRemoteLinkStep, createLinkStep, - removeRemoteLinkStep, useRemoteQueryStep, } from "../../common" 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 = { selector: ProductTypes.FilterableProductProps @@ -63,15 +63,16 @@ function updateProductIds({ updatedProducts: ProductTypes.ProductDTO[] input: WorkflowInput }) { + let productIds = updatedProducts.map((p) => p.id) + if ("products" in input) { - let productIds = updatedProducts.map((p) => p.id) const discardedProductIds: string[] = input.products .filter((p) => !p.sales_channels) .map((p) => p.id as string) return arrayDifference(productIds, discardedProductIds) } - return !input.update.sales_channels ? [] : undefined + return !input.update.sales_channels ? [] : productIds } function prepareSalesChannelLinks({ @@ -148,12 +149,12 @@ export const updateProductsWorkflow = createWorkflow( const currentLinks = useRemoteQueryStep({ entry_point: "product_sales_channel", fields: ["product_id", "sales_channel_id"], - variables: { product_id: updatedProductIds }, + variables: { filters: { product_id: updatedProductIds } }, }) const toDeleteLinks = transform({ currentLinks }, prepareToDeleteLinks) - removeRemoteLinkStep(toDeleteLinks as DeleteEntityInput[]) + dismissRemoteLinkStep(toDeleteLinks) const salesChannelLinks = transform( { input, updatedProducts }, diff --git a/packages/medusa/src/api/admin/products/validators.ts b/packages/medusa/src/api/admin/products/validators.ts index 5a2b2b006c..b7a760e9c0 100644 --- a/packages/medusa/src/api/admin/products/validators.ts +++ b/packages/medusa/src/api/admin/products/validators.ts @@ -42,6 +42,7 @@ export const AdminGetProductsParams = createFindParams({ .object({ variants: AdminGetProductVariantsParams.optional(), price_list_id: z.string().array().optional(), + status: statusEnum.array().optional(), $and: z.lazy(() => AdminGetProductsParams.array()).optional(), $or: z.lazy(() => AdminGetProductsParams.array()).optional(), })