diff --git a/integration-tests/api/__tests__/admin/sales-channels.js b/integration-tests/api/__tests__/admin/sales-channels.js index 663c0837c2..a849db72cb 100644 --- a/integration-tests/api/__tests__/admin/sales-channels.js +++ b/integration-tests/api/__tests__/admin/sales-channels.js @@ -2,6 +2,7 @@ const { ModuleRegistrationName } = require("@medusajs/modules-sdk") const { medusaIntegrationTestRunner } = require("medusa-test-utils") const { createAdminUser } = require("../../../helpers/create-admin-user") const { breaking } = require("../../../helpers/breaking") +const { ContainerRegistrationKeys } = require("@medusajs/utils") const adminReqConfig = { headers: { @@ -21,6 +22,8 @@ medusaIntegrationTestRunner({ testSuite: ({ dbConnection, getContainer, api }) => { let container let salesChannelService + let productService + let remoteQuery beforeAll(() => { ;({ @@ -41,6 +44,8 @@ medusaIntegrationTestRunner({ salesChannelService = container.resolve( ModuleRegistrationName.SALES_CHANNEL ) + productService = container.resolve(ModuleRegistrationName.PRODUCT) + remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY) }) describe("GET /admin/sales-channels/:id", () => { @@ -687,29 +692,59 @@ medusaIntegrationTestRunner({ }) describe("POST /admin/sales-channels/:id/products/batch", () => { - let salesChannel - let product + let { salesChannel, product } = {} beforeEach(async () => { - salesChannel = await simpleSalesChannelFactory(dbConnection, { - name: "test name", - description: "test description", - }) - product = await simpleProductFactory(dbConnection, { - id: "product_1", - title: "test title", - }) + ;({ salesChannel, product } = await breaking( + async () => { + const salesChannel = await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + }) + const product = await simpleProductFactory(dbConnection, { + id: "product_1", + title: "test title", + }) + + return { salesChannel, product } + }, + async () => { + const salesChannel = await salesChannelService.create({ + name: "test name", + description: "test description", + }) + const product = await productService.create({ + title: "test title", + }) + + return { salesChannel, product } + } + )) }) it("should add products to a sales channel", async () => { const payload = { - product_ids: [{ id: product.id }], + product_ids: breaking( + () => [{ id: product.id }], + () => [product.id] + ), } - const response = await api.post( - `/admin/sales-channels/${salesChannel.id}/products/batch`, - payload, - adminReqConfig + const response = await breaking( + async () => { + return await api.post( + `/admin/sales-channels/${salesChannel.id}/products/batch`, + payload, + adminReqConfig + ) + }, + async () => { + return await api.post( + `/admin/sales-channels/${salesChannel.id}/products/batch/add`, + payload, + adminReqConfig + ) + } ) expect(response.status).toEqual(200) @@ -725,26 +760,64 @@ medusaIntegrationTestRunner({ }) ) - const attachedProduct = await dbConnection.manager.findOne(Product, { - where: { id: product.id }, - relations: ["sales_channels"], - }) + const attachedProduct = await breaking( + async () => { + return await dbConnection.manager.findOne(Product, { + where: { id: product.id }, + relations: ["sales_channels"], + }) + }, + async () => { + const [product] = await remoteQuery({ + products: { + fields: ["id"], + sales_channels: { + fields: ["id", "name", "description", "is_disabled"], + }, + }, + }) + + return product + } + ) // + default sales channel - expect(attachedProduct.sales_channels.length).toBe(2) + expect(attachedProduct.sales_channels.length).toBe( + breaking( + () => 2, + () => 1 // Comment: The product factory from v1 adds products to the default channel + ) + ) expect(attachedProduct.sales_channels).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: "test name", - description: "test description", - is_disabled: false, - }), - expect.objectContaining({ - id: expect.any(String), - is_disabled: false, - }), - ]) + expect.arrayContaining( + breaking( + () => { + return [ + expect.objectContaining({ + id: expect.any(String), + name: "test name", + description: "test description", + is_disabled: false, + }), + expect.objectContaining({ + // Comment: Same as above + id: expect.any(String), + is_disabled: false, + }), + ] + }, + () => { + return [ + expect.objectContaining({ + id: expect.any(String), + name: "test name", + description: "test description", + is_disabled: false, + }), + ] + } + ) + ) ) }) }) 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 new file mode 100644 index 0000000000..d17d03d792 --- /dev/null +++ b/packages/core-flows/src/sales-channel/steps/associate-products-with-channels.ts @@ -0,0 +1,47 @@ +import { Modules } from "@medusajs/modules-sdk" +import { ContainerRegistrationKeys } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + links: { + sales_channel_id: string + product_ids: string[] + }[] +} + +export const associateProductsWithSalesChannelsStepId = + "associate-products-with-channels" +export const associateProductsWithSalesChannelsStep = createStep( + associateProductsWithSalesChannelsStepId, + async (input: StepInput, { container }) => { + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + 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 createdLinks = await remoteLink.create(links) + + return new StepResponse(createdLinks, links) + }, + async (links, { container }) => { + if (!links) { + return + } + + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + await remoteLink.dismiss(links) + } +) diff --git a/packages/core-flows/src/sales-channel/steps/index.ts b/packages/core-flows/src/sales-channel/steps/index.ts index 5e6727116d..cd51b9588a 100644 --- a/packages/core-flows/src/sales-channel/steps/index.ts +++ b/packages/core-flows/src/sales-channel/steps/index.ts @@ -1,3 +1,5 @@ +export * from "./associate-products-with-channels" export * from "./create-sales-channels" -export * from "./update-sales-channels" export * from "./delete-sales-channels" +export * from "./update-sales-channels" + 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 new file mode 100644 index 0000000000..ce6efd164f --- /dev/null +++ b/packages/core-flows/src/sales-channel/workflows/add-products-to-sales-channels.ts @@ -0,0 +1,19 @@ +import { SalesChannelDTO } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { associateProductsWithSalesChannelsStep } from "../steps/associate-products-with-channels" + +type WorkflowInput = { + data: { + sales_channel_id: string + product_ids: string[] + }[] +} + +export const addProductsToSalesChannelsWorkflowId = + "add-products-to-sales-channels" +export const addProductsToSalesChannelsWorkflow = createWorkflow( + addProductsToSalesChannelsWorkflowId, + (input: WorkflowData): WorkflowData => { + return associateProductsWithSalesChannelsStep({ links: input.data }) + } +) diff --git a/packages/core-flows/src/sales-channel/workflows/index.ts b/packages/core-flows/src/sales-channel/workflows/index.ts index 5e6727116d..e00575b44e 100644 --- a/packages/core-flows/src/sales-channel/workflows/index.ts +++ b/packages/core-flows/src/sales-channel/workflows/index.ts @@ -1,3 +1,5 @@ +export * from "./add-products-to-sales-channels" export * from "./create-sales-channels" -export * from "./update-sales-channels" export * from "./delete-sales-channels" +export * from "./update-sales-channels" + diff --git a/packages/medusa/src/api-v2/admin/sales-channels/[id]/products/batch/add/route.ts b/packages/medusa/src/api-v2/admin/sales-channels/[id]/products/batch/add/route.ts new file mode 100644 index 0000000000..d2eb1580d5 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/sales-channels/[id]/products/batch/add/route.ts @@ -0,0 +1,49 @@ +import { addProductsToSalesChannelsWorkflow } from "@medusajs/core-flows" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../../../types/routing" +import { defaultAdminSalesChannelFields } from "../../../../query-config" +import { AdminPostSalesChannelsChannelProductsBatchReq } from "../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const body = + req.validatedBody as AdminPostSalesChannelsChannelProductsBatchReq + + const workflowInput = { + data: [ + { + sales_channel_id: req.params.id, + product_ids: body.product_ids, + }, + ], + } + + const { errors } = await addProductsToSalesChannelsWorkflow(req.scope).run({ + input: workflowInput, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "sales_channels", + variables: { id: req.params.id }, + fields: defaultAdminSalesChannelFields, + }) + + const [sales_channel] = await remoteQuery(queryObject) + + res.status(200).json({ sales_channel }) +} diff --git a/packages/medusa/src/api-v2/admin/sales-channels/middlewares.ts b/packages/medusa/src/api-v2/admin/sales-channels/middlewares.ts index 08fae4a663..13d31fe29f 100644 --- a/packages/medusa/src/api-v2/admin/sales-channels/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/sales-channels/middlewares.ts @@ -5,6 +5,7 @@ import * as QueryConfig from "./query-config" import { AdminGetSalesChannelsParams, AdminGetSalesChannelsSalesChannelParams, + AdminPostSalesChannelsChannelProductsBatchReq, AdminPostSalesChannelsReq, AdminPostSalesChannelsSalesChannelReq, } from "./validators" @@ -56,4 +57,9 @@ export const adminSalesChannelRoutesMiddlewares: MiddlewareRoute[] = [ matcher: "/admin/sales-channels/:id", middlewares: [], }, + { + method: ["POST"], + matcher: "/admin/sales-channels/:id/products/batch/add", + middlewares: [transformBody(AdminPostSalesChannelsChannelProductsBatchReq)], + }, ] diff --git a/packages/medusa/src/api-v2/admin/sales-channels/validators.ts b/packages/medusa/src/api-v2/admin/sales-channels/validators.ts index 2ec35b3f79..110c92299b 100644 --- a/packages/medusa/src/api-v2/admin/sales-channels/validators.ts +++ b/packages/medusa/src/api-v2/admin/sales-channels/validators.ts @@ -1,6 +1,7 @@ import { OperatorMap } from "@medusajs/types" import { Type } from "class-transformer" import { + IsArray, IsBoolean, IsNotEmpty, IsOptional, @@ -100,3 +101,8 @@ export class AdminPostSalesChannelsSalesChannelReq { @IsOptional() metadata?: Record } + +export class AdminPostSalesChannelsChannelProductsBatchReq { + @IsArray() + product_ids: string[] +}