From 44bcde92c88672d316623f9aa7b3abd2b7f96cb1 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Sun, 7 Apr 2024 13:45:47 +0200 Subject: [PATCH] feat: Add support for setting sales channel when creating a product (#6986) --- .../api/__tests__/admin/product.js | 81 ++++++------------- .../src/common/steps/remove-remote-links.ts | 2 +- .../core-flows/src/product/steps/index.ts | 1 - .../steps/remove-variant-pricing-link.ts | 33 -------- .../src/product/workflows/create-products.ts | 42 +++++----- .../workflows/delete-product-variants.ts | 12 +-- .../src/product/workflows/delete-products.ts | 17 ++-- .../src/product/workflows/update-products.ts | 1 + .../steps/associate-products-with-channels.ts | 32 ++++---- .../detach-products-from-sales-channels.ts | 31 ++++--- .../add-products-to-sales-channels.ts | 16 +++- .../remove-products-from-sales-channels.ts | 16 +++- .../src/api-v2/admin/products/validators.ts | 10 +-- 13 files changed, 126 insertions(+), 168 deletions(-) delete mode 100644 packages/core-flows/src/product/steps/remove-variant-pricing-link.ts diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index c4acd89f97..d6d4075b6f 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -9,7 +9,6 @@ const { createVariantPriceSet, } = require("../../../modules/helpers/create-variant-price-set") const { PriceListStatus, PriceListType } = require("@medusajs/types") -const { ContainerRegistrationKeys } = require("@medusajs/utils") let { ProductOptionValue, @@ -85,12 +84,9 @@ medusaIntegrationTestRunner({ let publishedCollection let baseType - let baseRegion let pricingService - let scService - let remoteLink let container beforeAll(() => { @@ -219,20 +215,10 @@ medusaIntegrationTestRunner({ await api.delete(`/admin/products/${deletedProduct.id}`, adminHeaders) pricingService = container.resolve(ModuleRegistrationName.PRICING) - scService = container.resolve(ModuleRegistrationName.SALES_CHANNEL) - remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) }) describe("/admin/products", () => { describe("GET /admin/products", () => { - beforeEach(async () => { - await simpleSalesChannelFactory(dbConnection, { - name: "Default channel", - id: "default-channel", - is_default: true, - }) - }) - it("returns a list of products with all statuses when no status or invalid status is provided", async () => { const res = await api .get("/admin/products", adminHeaders) @@ -1057,7 +1043,7 @@ medusaIntegrationTestRunner({ }) it("should return products filtered by sales_channel_id", async () => { - const { salesChannel } = await breaking( + const [productId, salesChannelId] = await breaking( async () => { const salesChannel = await simpleSalesChannelFactory( dbConnection, @@ -1068,29 +1054,34 @@ medusaIntegrationTestRunner({ } ) - return { salesChannel } + return [baseProduct.id, salesChannel.id] }, async () => { - const salesChannel = await scService.create({ - name: "Test channel", - description: "Lorem Ipsum", - }) + const salesChannel = await simpleSalesChannelFactory( + dbConnection, + { + name: "test name", + description: "test description", + } + ) - await remoteLink.create({ - [Modules.PRODUCT]: { - product_id: baseProduct.id, - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: salesChannel.id, - }, - }) - - return { salesChannel } + // Currently the product update doesn't support managing sales channels + const newProduct = ( + await api.post( + "/admin/products", + getProductFixture({ + title: "Test saleschannel", + sales_channels: [{ id: salesChannel.id }], + }), + adminHeaders + ) + ).data.product + return [newProduct.id, salesChannel.id] } ) const res = await api.get( - `/admin/products?sales_channel_id[]=${salesChannel.id}`, + `/admin/products?sales_channel_id[]=${salesChannelId}`, adminHeaders ) @@ -1098,8 +1089,7 @@ medusaIntegrationTestRunner({ expect(res.data.products.length).toEqual(1) expect(res.data.products).toEqual([ expect.objectContaining({ - id: baseProduct.id, - status: "draft", + id: productId, }), ]) }) @@ -1216,14 +1206,6 @@ medusaIntegrationTestRunner({ }) describe("POST /admin/products", () => { - beforeEach(async () => { - await simpleSalesChannelFactory(dbConnection, { - name: "Default channel", - id: "default-channel", - is_default: true, - }) - }) - it("creates a product", async () => { const response = await api .post( @@ -1957,15 +1939,6 @@ medusaIntegrationTestRunner({ }) describe("updates a variant's default prices (ignores prices associated with a Price List)", () => { - beforeEach(async () => { - // await priceListSeeder(dbConnection) - await simpleSalesChannelFactory(dbConnection, { - name: "Default channel", - id: "default-channel", - is_default: true, - }) - }) - it("successfully updates a variant's default prices by changing an existing price (currency_code)", async () => { const data = { prices: [ @@ -2843,14 +2816,6 @@ medusaIntegrationTestRunner({ // TODO: Discuss how this should be handled describe.skip("GET /admin/products/tag-usage", () => { - beforeEach(async () => { - await simpleSalesChannelFactory(dbConnection, { - name: "Default channel", - id: "default-channel", - is_default: true, - }) - }) - it("successfully gets the tags usage", async () => { const res = await api .get("/admin/products/tag-usage", adminHeaders) diff --git a/packages/core-flows/src/common/steps/remove-remote-links.ts b/packages/core-flows/src/common/steps/remove-remote-links.ts index 12fda3b843..0aeb906463 100644 --- a/packages/core-flows/src/common/steps/remove-remote-links.ts +++ b/packages/core-flows/src/common/steps/remove-remote-links.ts @@ -35,7 +35,7 @@ export const removeRemoteLinkStep = createStep( ) await link.delete(grouped) - return new StepResponse(void 0, grouped) + return new StepResponse(grouped, grouped) }, async (removedLinks, { container }) => { if (!removedLinks) { diff --git a/packages/core-flows/src/product/steps/index.ts b/packages/core-flows/src/product/steps/index.ts index 10a1e673b9..6054d82f45 100644 --- a/packages/core-flows/src/product/steps/index.ts +++ b/packages/core-flows/src/product/steps/index.ts @@ -3,7 +3,6 @@ export * from "./update-products" export * from "./delete-products" export * from "./get-products" export * from "./create-variant-pricing-link" -export * from "./remove-variant-pricing-link" export * from "./create-product-options" export * from "./update-product-options" export * from "./delete-product-options" diff --git a/packages/core-flows/src/product/steps/remove-variant-pricing-link.ts b/packages/core-flows/src/product/steps/remove-variant-pricing-link.ts deleted file mode 100644 index 7599c37490..0000000000 --- a/packages/core-flows/src/product/steps/remove-variant-pricing-link.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Modules } from "@medusajs/modules-sdk" -import { ContainerRegistrationKeys } from "@medusajs/utils" -import { StepResponse, createStep } from "@medusajs/workflows-sdk" - -type StepInput = { - variant_ids: string[] -} - -export const removeVariantPricingLinkStepId = "remove-variant-pricing-link" -export const removeVariantPricingLinkStep = createStep( - removeVariantPricingLinkStepId, - async (data: StepInput, { container }) => { - if (!data.variant_ids.length) { - return - } - - const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) - await remoteLink.delete({ - [Modules.PRODUCT]: { variant_id: data.variant_ids }, - }) - return new StepResponse(void 0, data.variant_ids) - }, - async (prevData, { container }) => { - if (!prevData?.length) { - return - } - - const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) - await remoteLink.restore({ - [Modules.PRODUCT]: { variant_id: prevData }, - }) - } -) diff --git a/packages/core-flows/src/product/workflows/create-products.ts b/packages/core-flows/src/product/workflows/create-products.ts index 60f4e97dcc..f4f180fe8c 100644 --- a/packages/core-flows/src/product/workflows/create-products.ts +++ b/packages/core-flows/src/product/workflows/create-products.ts @@ -6,11 +6,13 @@ import { } from "@medusajs/workflows-sdk" import { createProductsStep, createVariantPricingLinkStep } from "../steps" import { createPriceSetsStep } from "../../pricing" +import { associateProductsWithSalesChannelsStep } from "../../sales-channel" // TODO: We should have separate types here as input, not the module DTO. Eg. the HTTP request that we are handling // has different data than the DTO, so that needs to be represented differently. type WorkflowInput = { products: (Omit & { + sales_channels?: { id: string }[] variants?: (ProductTypes.CreateProductVariantDTO & { prices?: PricingTypes.CreateMoneyAmountDTO[] })[] @@ -24,9 +26,10 @@ export const createProductsWorkflow = createWorkflow( input: WorkflowData ): WorkflowData => { // Passing prices to the product module will fail, we want to keep them for after the product is created. - const productWithoutPrices = transform({ input }, (data) => + const productWithoutExternalRelations = transform({ input }, (data) => data.input.products.map((p) => ({ ...p, + sales_channels: undefined, variants: p.variants?.map((v) => ({ ...v, prices: undefined, @@ -34,7 +37,23 @@ export const createProductsWorkflow = createWorkflow( })) ) - const createdProducts = createProductsStep(productWithoutPrices) + const createdProducts = createProductsStep(productWithoutExternalRelations) + + const salesChannelLinks = transform({ input, createdProducts }, (data) => { + return data.createdProducts + .map((createdProduct, i) => { + const inputProduct = data.input.products[i] + return ( + inputProduct.sales_channels?.map((salesChannel) => ({ + sales_channel_id: salesChannel.id, + product_id: createdProduct.id, + })) ?? [] + ) + }) + .flat() + }) + + associateProductsWithSalesChannelsStep({ links: salesChannelLinks }) // Note: We rely on the same order of input and output when creating products here, ensure this always holds true const variantsWithAssociatedPrices = transform( @@ -83,23 +102,6 @@ export const createProductsWorkflow = createWorkflow( createVariantPricingLinkStep(variantAndPriceSetLinks) - // TODO: Should we just refetch the products here? - return transform( - { - createdProducts, - variantAndPriceSets, - }, - (data) => { - return data.createdProducts.map((product) => ({ - ...product, - variants: product.variants?.map((variant) => ({ - ...variant, - price_set: data.variantAndPriceSets.find( - (v) => v.variant.id === variant.id - )?.price_set, - })), - })) - } - ) + return createdProducts } ) diff --git a/packages/core-flows/src/product/workflows/delete-product-variants.ts b/packages/core-flows/src/product/workflows/delete-product-variants.ts index 635dad670d..00aec64bdd 100644 --- a/packages/core-flows/src/product/workflows/delete-product-variants.ts +++ b/packages/core-flows/src/product/workflows/delete-product-variants.ts @@ -1,8 +1,7 @@ import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { - deleteProductVariantsStep, - removeVariantPricingLinkStep, -} from "../steps" +import { Modules } from "@medusajs/modules-sdk" +import { deleteProductVariantsStep } from "../steps" +import { removeRemoteLinkStep } from "../../common" type WorkflowInput = { ids: string[] } @@ -10,7 +9,10 @@ export const deleteProductVariantsWorkflowId = "delete-product-variants" export const deleteProductVariantsWorkflow = createWorkflow( deleteProductVariantsWorkflowId, (input: WorkflowData): WorkflowData => { - removeVariantPricingLinkStep({ variant_ids: input.ids }) + removeRemoteLinkStep({ + [Modules.PRODUCT]: { variant_id: input.ids }, + }).config({ name: "remove-variant-link-step" }) + return deleteProductVariantsStep(input.ids) } ) diff --git a/packages/core-flows/src/product/workflows/delete-products.ts b/packages/core-flows/src/product/workflows/delete-products.ts index fae70f2c18..27522d103f 100644 --- a/packages/core-flows/src/product/workflows/delete-products.ts +++ b/packages/core-flows/src/product/workflows/delete-products.ts @@ -3,11 +3,9 @@ import { createWorkflow, transform, } from "@medusajs/workflows-sdk" -import { - deleteProductsStep, - getProductsStep, - removeVariantPricingLinkStep, -} from "../steps" +import { Modules } from "@medusajs/modules-sdk" +import { deleteProductsStep, getProductsStep } from "../steps" +import { removeRemoteLinkStep } from "../../common" type WorkflowInput = { ids: string[] } @@ -22,7 +20,14 @@ export const deleteProductsWorkflow = createWorkflow( .map((variant) => variant.id) }) - removeVariantPricingLinkStep({ variant_ids: variantsToBeDeleted }) + removeRemoteLinkStep({ + [Modules.PRODUCT]: { variant_id: variantsToBeDeleted }, + }).config({ name: "remove-variant-link-step" }) + + removeRemoteLinkStep({ + [Modules.PRODUCT]: { product_id: input.ids }, + }).config({ name: "remove-product-link-step" }) + return deleteProductsStep(input.ids) } ) diff --git a/packages/core-flows/src/product/workflows/update-products.ts b/packages/core-flows/src/product/workflows/update-products.ts index 12799fc980..7ab2266100 100644 --- a/packages/core-flows/src/product/workflows/update-products.ts +++ b/packages/core-flows/src/product/workflows/update-products.ts @@ -16,6 +16,7 @@ export const updateProductsWorkflow = createWorkflow( input: WorkflowData ): WorkflowData => { // TODO: Delete price sets for removed variants + // TODO Update sales channel links return updateProductsStep(input) } ) diff --git a/packages/core-flows/src/sales-channel/steps/associate-products-with-channels.ts b/packages/core-flows/src/sales-channel/steps/associate-products-with-channels.ts index d17d03d792..54059c65a8 100644 --- a/packages/core-flows/src/sales-channel/steps/associate-products-with-channels.ts +++ b/packages/core-flows/src/sales-channel/steps/associate-products-with-channels.ts @@ -5,7 +5,7 @@ import { StepResponse, createStep } from "@medusajs/workflows-sdk" interface StepInput { links: { sales_channel_id: string - product_ids: string[] + product_id: string }[] } @@ -14,25 +14,23 @@ export const associateProductsWithSalesChannelsStepId = export const associateProductsWithSalesChannelsStep = createStep( associateProductsWithSalesChannelsStepId, async (input: StepInput, { container }) => { - const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + if (!input.links.length) { + return new StepResponse([], []) + } - const links = input.links - .map((link) => { - return link.product_ids.map((id) => { - return { - [Modules.PRODUCT]: { - product_id: id, - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: link.sales_channel_id, - }, - } - }) - }) - .flat() + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + const links = input.links.map((link) => { + return { + [Modules.PRODUCT]: { + product_id: link.product_id, + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: link.sales_channel_id, + }, + } + }) const createdLinks = await remoteLink.create(links) - return new StepResponse(createdLinks, links) }, async (links, { container }) => { diff --git a/packages/core-flows/src/sales-channel/steps/detach-products-from-sales-channels.ts b/packages/core-flows/src/sales-channel/steps/detach-products-from-sales-channels.ts index 74f8af1a7a..548b857351 100644 --- a/packages/core-flows/src/sales-channel/steps/detach-products-from-sales-channels.ts +++ b/packages/core-flows/src/sales-channel/steps/detach-products-from-sales-channels.ts @@ -5,7 +5,7 @@ import { StepResponse, createStep } from "@medusajs/workflows-sdk" interface StepInput { links: { sales_channel_id: string - product_ids: string[] + product_id: string }[] } @@ -14,22 +14,21 @@ export const detachProductsFromSalesChannelsStepId = export const detachProductsFromSalesChannelsStep = createStep( detachProductsFromSalesChannelsStepId, async (input: StepInput, { container }) => { - const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + if (!input.links.length) { + return new StepResponse(void 0, []) + } - const links = input.links - .map((link) => { - return link.product_ids.map((id) => { - return { - [Modules.PRODUCT]: { - product_id: id, - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: link.sales_channel_id, - }, - } - }) - }) - .flat() + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + const links = input.links.map((link) => { + return { + [Modules.PRODUCT]: { + product_id: link.product_id, + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: link.sales_channel_id, + }, + } + }) await remoteLink.dismiss(links) diff --git a/packages/core-flows/src/sales-channel/workflows/add-products-to-sales-channels.ts b/packages/core-flows/src/sales-channel/workflows/add-products-to-sales-channels.ts index ce6efd164f..103c3ab38a 100644 --- a/packages/core-flows/src/sales-channel/workflows/add-products-to-sales-channels.ts +++ b/packages/core-flows/src/sales-channel/workflows/add-products-to-sales-channels.ts @@ -1,6 +1,7 @@ import { SalesChannelDTO } from "@medusajs/types" import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" import { associateProductsWithSalesChannelsStep } from "../steps/associate-products-with-channels" +import { transform } from "@medusajs/workflows-sdk" type WorkflowInput = { data: { @@ -14,6 +15,19 @@ export const addProductsToSalesChannelsWorkflowId = export const addProductsToSalesChannelsWorkflow = createWorkflow( addProductsToSalesChannelsWorkflowId, (input: WorkflowData): WorkflowData => { - return associateProductsWithSalesChannelsStep({ links: input.data }) + const links = transform({ input }, (data) => { + return data.input.data + .map(({ sales_channel_id, product_ids }) => { + return product_ids.map((product_id) => { + return { + sales_channel_id, + product_id, + } + }) + }) + .flat() + }) + + return associateProductsWithSalesChannelsStep({ links }) } ) diff --git a/packages/core-flows/src/sales-channel/workflows/remove-products-from-sales-channels.ts b/packages/core-flows/src/sales-channel/workflows/remove-products-from-sales-channels.ts index 325c5842a8..87627762f6 100644 --- a/packages/core-flows/src/sales-channel/workflows/remove-products-from-sales-channels.ts +++ b/packages/core-flows/src/sales-channel/workflows/remove-products-from-sales-channels.ts @@ -1,6 +1,7 @@ import { SalesChannelDTO } from "@medusajs/types" import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" import { detachProductsFromSalesChannelsStep } from "../steps/detach-products-from-sales-channels" +import { transform } from "@medusajs/workflows-sdk" type WorkflowInput = { data: { @@ -14,6 +15,19 @@ export const removeProductsFromSalesChannelsWorkflowId = export const removeProductsFromSalesChannelsWorkflow = createWorkflow( removeProductsFromSalesChannelsWorkflowId, (input: WorkflowData): WorkflowData => { - return detachProductsFromSalesChannelsStep({ links: input.data }) + const links = transform({ input }, (data) => { + return data.input.data + .map(({ sales_channel_id, product_ids }) => { + return product_ids.map((product_id) => { + return { + sales_channel_id, + product_id, + } + }) + }) + .flat() + }) + + return detachProductsFromSalesChannelsStep({ links }) } ) diff --git a/packages/medusa/src/api-v2/admin/products/validators.ts b/packages/medusa/src/api-v2/admin/products/validators.ts index b0649760f9..d939211a57 100644 --- a/packages/medusa/src/api-v2/admin/products/validators.ts +++ b/packages/medusa/src/api-v2/admin/products/validators.ts @@ -202,6 +202,7 @@ export const AdminCreateProduct = z tags: z.array(AdminUpdateProductTag).optional(), options: z.array(AdminCreateProductOption).optional(), variants: z.array(AdminCreateProductVariant).optional(), + sales_channels: z.array(z.object({ id: z.string() })).optional(), weight: z.number().optional(), length: z.number().optional(), height: z.number().optional(), @@ -231,12 +232,3 @@ export const AdminUpdateProduct = AdminCreateProduct.omit({ is_giftcard: true }) // @ValidateNested({ each: true }) // @IsArray() // categories?: ProductProductCategoryReq[] - -// TODO: Deal with in next iteration -// @FeatureFlagDecorators(SalesChannelFeatureFlag.key, [ -// IsOptional(), -// Type(() => ProductSalesChannelReq), -// ValidateNested({ each: true }), -// IsArray(), -// ]) -// sales_channels?: ProductSalesChannelReq[]