From b7df44768295ef404bbd93c8b20b1c7b1e534f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Tue, 21 May 2024 21:48:34 +0200 Subject: [PATCH] fix(core-flows): set SalesChannels on Product update (#7272) --- .changeset/spicy-panthers-exist.md | 5 + .../api/__tests__/admin/product.js | 117 +++++++++++++ .../src/product/steps/update-products.ts | 2 +- .../src/product/workflows/update-products.ts | 161 ++++++++++++++++-- .../steps/associate-products-with-channels.ts | 2 +- 5 files changed, 275 insertions(+), 12 deletions(-) create mode 100644 .changeset/spicy-panthers-exist.md diff --git a/.changeset/spicy-panthers-exist.md b/.changeset/spicy-panthers-exist.md new file mode 100644 index 0000000000..a63dbaf6f6 --- /dev/null +++ b/.changeset/spicy-panthers-exist.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +fix(core-flows): set SalesChannels for Products on update diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 68294d1367..259415fb20 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -1749,6 +1749,123 @@ medusaIntegrationTestRunner({ ) }) + it("updates products sales channels", async () => { + const [productId, salesChannel1Id, salesChannel2Id] = await breaking( + async () => { + const salesChannel1 = await simpleSalesChannelFactory( + dbConnection, + { + name: "test name 1", + description: "test description", + products: [baseProduct], + } + ) + + const salesChannel2 = await simpleSalesChannelFactory( + dbConnection, + { + name: "test name 2", + description: "test description", + salesChannel1, + // no assigned products + } + ) + + return [baseProduct.id, salesChannel1.id, salesChannel2.id] + }, + async () => { + const salesChannelService = getContainer().resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + + const salesChannel1 = await salesChannelService.create({ + name: "test name 1", + description: "test description", + }) + + const salesChannel2 = await salesChannelService.create({ + name: "test name 2", + description: "test description", + }) + + const newProduct = ( + await api.post( + "/admin/products", + getProductFixture({ + title: "Test saleschannel", + sales_channels: [{ id: salesChannel1.id }], + }), + adminHeaders + ) + ).data.product + return [newProduct.id, salesChannel1.id, salesChannel2.id] + } + ) + + await api.post( + `/admin/products/${productId}`, + { + title: "new name", + sales_channels: [ + { id: salesChannel1Id }, + { id: salesChannel2Id }, + ], + }, + adminHeaders + ) + + let res = await api.get( + `/admin/products/${productId}?fields=*sales_channels`, + adminHeaders + ) + + expect(res.status).toEqual(200) + expect(res.data.product).toEqual( + expect.objectContaining({ + id: productId, + title: "new name", + sales_channels: expect.arrayContaining([ + expect.objectContaining({ + id: salesChannel1Id, + name: "test name 1", + }), + expect.objectContaining({ + id: salesChannel2Id, + name: "test name 2", + }), + ]), + }) + ) + + await api.post( + `/admin/products/${productId}`, + { + title: "new name 2", + sales_channels: [{ id: salesChannel2Id }], // update channels again to remove the first one + }, + adminHeaders + ) + + res = await api.get( + `/admin/products/${productId}?fields=*sales_channels`, + adminHeaders + ) + + expect(res.status).toEqual(200) + expect(res.data.product).toEqual( + expect.objectContaining({ + id: productId, + title: "new name 2", + sales_channels: expect.arrayContaining([ + expect.objectContaining({ + id: salesChannel2Id, + name: "test name 2", + }), + ]), + }) + ) + }) + it("fails to update product with invalid status", async () => { const payload = { status: null, diff --git a/packages/core/core-flows/src/product/steps/update-products.ts b/packages/core/core-flows/src/product/steps/update-products.ts index 4ad57e416f..c87247ce7f 100644 --- a/packages/core/core-flows/src/product/steps/update-products.ts +++ b/packages/core/core-flows/src/product/steps/update-products.ts @@ -6,7 +6,7 @@ import { } from "@medusajs/utils" import { StepResponse, createStep } from "@medusajs/workflows-sdk" -type UpdateProductsStepInput = +export type UpdateProductsStepInput = | { selector: ProductTypes.FilterableProductProps update: ProductTypes.UpdateProductDTO 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 6e1312b632..b4d6456f75 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,135 @@ import { ProductTypes } from "@medusajs/types" -import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { + createWorkflow, + transform, + WorkflowData, +} from "@medusajs/workflows-sdk" import { updateProductsStep } from "../steps/update-products" +import { + createLinkStep, + removeRemoteLinkStep, + useRemoteQueryStep, +} from "../../common" +import { arrayDifference } from "@medusajs/utils" +import { DeleteEntityInput, Modules } from "@medusajs/modules-sdk" + +type UpdateProductsStepInputSelector = { + selector: ProductTypes.FilterableProductProps + update: ProductTypes.UpdateProductDTO & { + sales_channels?: { id: string }[] + } +} + +type UpdateProductsStepInputProducts = { + products: (ProductTypes.UpsertProductDTO & { + sales_channels?: { id: string }[] + })[] +} + type UpdateProductsStepInput = - | { - selector: ProductTypes.FilterableProductProps - update: ProductTypes.UpdateProductDTO - } - | { - products: ProductTypes.UpsertProductDTO[] - } + | UpdateProductsStepInputSelector + | UpdateProductsStepInputProducts type WorkflowInput = UpdateProductsStepInput +function prepareUpdateProductInput({ + input, +}: { + input: WorkflowInput +}): UpdateProductsStepInput { + if ("products" in input) { + return { + products: input.products.map((p) => ({ + ...p, + sales_channels: undefined, + })), + } + } + + return { + selector: input.selector, + update: { + ...input.update, + sales_channels: undefined, + }, + } +} + +function updateProductIds({ + updatedProducts, + input, +}: { + updatedProducts: ProductTypes.ProductDTO[] + input: WorkflowInput +}) { + 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 +} + +function prepareSalesChannelLinks({ + input, + updatedProducts, +}: { + updatedProducts: ProductTypes.ProductDTO[] + input: WorkflowInput +}): Record>[] { + if ("products" in input) { + return input.products + .filter((p) => p.sales_channels) + .flatMap((p) => + p.sales_channels!.map((sc) => ({ + [Modules.PRODUCT]: { + product_id: p.id, + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: sc.id, + }, + })) + ) + } + + if (input.selector && input.update.sales_channels?.length) { + return updatedProducts.flatMap((p) => + input.update.sales_channels!.map((channel) => ({ + [Modules.PRODUCT]: { + product_id: p.id, + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: channel.id, + }, + })) + ) + } + + return [] +} + +function prepareToDeleteLinks({ + currentLinks, +}: { + currentLinks: { + product_id: string + sales_channel_id: string + }[] +}) { + return currentLinks.map(({ product_id, sales_channel_id }) => ({ + [Modules.PRODUCT]: { + product_id, + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id, + }, + })) +} + export const updateProductsWorkflowId = "update-products" export const updateProductsWorkflow = createWorkflow( updateProductsWorkflowId, @@ -20,7 +137,31 @@ export const updateProductsWorkflow = createWorkflow( input: WorkflowData ): WorkflowData => { // TODO: Delete price sets for removed variants - // TODO Update sales channel links - return updateProductsStep(input) + + const toUpdateInput = transform({ input }, prepareUpdateProductInput) + const updatedProducts = updateProductsStep(toUpdateInput) + const updatedProductIds = transform( + { updatedProducts, input }, + updateProductIds + ) + + const currentLinks = useRemoteQueryStep({ + entry_point: "product_sales_channel", + fields: ["product_id", "sales_channel_id"], + variables: { product_id: updatedProductIds }, + }) + + const toDeleteLinks = transform({ currentLinks }, prepareToDeleteLinks) + + removeRemoteLinkStep(toDeleteLinks as DeleteEntityInput[]) + + const salesChannelLinks = transform( + { input, updatedProducts }, + prepareSalesChannelLinks + ) + + createLinkStep(salesChannelLinks) + + return updatedProducts } ) diff --git a/packages/core/core-flows/src/sales-channel/steps/associate-products-with-channels.ts b/packages/core/core-flows/src/sales-channel/steps/associate-products-with-channels.ts index 2cb163e4c8..25dafc1697 100644 --- a/packages/core/core-flows/src/sales-channel/steps/associate-products-with-channels.ts +++ b/packages/core/core-flows/src/sales-channel/steps/associate-products-with-channels.ts @@ -1,6 +1,6 @@ import { Modules } from "@medusajs/modules-sdk" import { ContainerRegistrationKeys } from "@medusajs/utils" -import { StepResponse, createStep } from "@medusajs/workflows-sdk" +import { createStep, StepResponse } from "@medusajs/workflows-sdk" interface StepInput { links: {