feat: Add products to sales channel (#6725)

Depends on #6722
This commit is contained in:
Oli Juhl
2024-03-19 10:24:37 +00:00
committed by GitHub
parent 62d5803b20
commit 3062605bce
8 changed files with 238 additions and 34 deletions
@@ -2,6 +2,7 @@ const { ModuleRegistrationName } = require("@medusajs/modules-sdk")
const { medusaIntegrationTestRunner } = require("medusa-test-utils") const { medusaIntegrationTestRunner } = require("medusa-test-utils")
const { createAdminUser } = require("../../../helpers/create-admin-user") const { createAdminUser } = require("../../../helpers/create-admin-user")
const { breaking } = require("../../../helpers/breaking") const { breaking } = require("../../../helpers/breaking")
const { ContainerRegistrationKeys } = require("@medusajs/utils")
const adminReqConfig = { const adminReqConfig = {
headers: { headers: {
@@ -21,6 +22,8 @@ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => { testSuite: ({ dbConnection, getContainer, api }) => {
let container let container
let salesChannelService let salesChannelService
let productService
let remoteQuery
beforeAll(() => { beforeAll(() => {
;({ ;({
@@ -41,6 +44,8 @@ medusaIntegrationTestRunner({
salesChannelService = container.resolve( salesChannelService = container.resolve(
ModuleRegistrationName.SALES_CHANNEL ModuleRegistrationName.SALES_CHANNEL
) )
productService = container.resolve(ModuleRegistrationName.PRODUCT)
remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
}) })
describe("GET /admin/sales-channels/:id", () => { describe("GET /admin/sales-channels/:id", () => {
@@ -687,29 +692,59 @@ medusaIntegrationTestRunner({
}) })
describe("POST /admin/sales-channels/:id/products/batch", () => { describe("POST /admin/sales-channels/:id/products/batch", () => {
let salesChannel let { salesChannel, product } = {}
let product
beforeEach(async () => { beforeEach(async () => {
salesChannel = await simpleSalesChannelFactory(dbConnection, { ;({ salesChannel, product } = await breaking(
name: "test name", async () => {
description: "test description", const salesChannel = await simpleSalesChannelFactory(dbConnection, {
}) name: "test name",
product = await simpleProductFactory(dbConnection, { description: "test description",
id: "product_1", })
title: "test title", 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 () => { it("should add products to a sales channel", async () => {
const payload = { const payload = {
product_ids: [{ id: product.id }], product_ids: breaking(
() => [{ id: product.id }],
() => [product.id]
),
} }
const response = await api.post( const response = await breaking(
`/admin/sales-channels/${salesChannel.id}/products/batch`, async () => {
payload, return await api.post(
adminReqConfig `/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) expect(response.status).toEqual(200)
@@ -725,26 +760,64 @@ medusaIntegrationTestRunner({
}) })
) )
const attachedProduct = await dbConnection.manager.findOne(Product, { const attachedProduct = await breaking(
where: { id: product.id }, async () => {
relations: ["sales_channels"], 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 // + 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(attachedProduct.sales_channels).toEqual(
expect.arrayContaining([ expect.arrayContaining(
expect.objectContaining({ breaking(
id: expect.any(String), () => {
name: "test name", return [
description: "test description", expect.objectContaining({
is_disabled: false, id: expect.any(String),
}), name: "test name",
expect.objectContaining({ description: "test description",
id: expect.any(String), is_disabled: false,
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,
}),
]
}
)
)
) )
}) })
}) })
@@ -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)
}
)
@@ -1,3 +1,5 @@
export * from "./associate-products-with-channels"
export * from "./create-sales-channels" export * from "./create-sales-channels"
export * from "./update-sales-channels"
export * from "./delete-sales-channels" export * from "./delete-sales-channels"
export * from "./update-sales-channels"
@@ -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<WorkflowInput>): WorkflowData<SalesChannelDTO[]> => {
return associateProductsWithSalesChannelsStep({ links: input.data })
}
)
@@ -1,3 +1,5 @@
export * from "./add-products-to-sales-channels"
export * from "./create-sales-channels" export * from "./create-sales-channels"
export * from "./update-sales-channels"
export * from "./delete-sales-channels" export * from "./delete-sales-channels"
export * from "./update-sales-channels"
@@ -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 })
}
@@ -5,6 +5,7 @@ import * as QueryConfig from "./query-config"
import { import {
AdminGetSalesChannelsParams, AdminGetSalesChannelsParams,
AdminGetSalesChannelsSalesChannelParams, AdminGetSalesChannelsSalesChannelParams,
AdminPostSalesChannelsChannelProductsBatchReq,
AdminPostSalesChannelsReq, AdminPostSalesChannelsReq,
AdminPostSalesChannelsSalesChannelReq, AdminPostSalesChannelsSalesChannelReq,
} from "./validators" } from "./validators"
@@ -56,4 +57,9 @@ export const adminSalesChannelRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/sales-channels/:id", matcher: "/admin/sales-channels/:id",
middlewares: [], middlewares: [],
}, },
{
method: ["POST"],
matcher: "/admin/sales-channels/:id/products/batch/add",
middlewares: [transformBody(AdminPostSalesChannelsChannelProductsBatchReq)],
},
] ]
@@ -1,6 +1,7 @@
import { OperatorMap } from "@medusajs/types" import { OperatorMap } from "@medusajs/types"
import { Type } from "class-transformer" import { Type } from "class-transformer"
import { import {
IsArray,
IsBoolean, IsBoolean,
IsNotEmpty, IsNotEmpty,
IsOptional, IsOptional,
@@ -100,3 +101,8 @@ export class AdminPostSalesChannelsSalesChannelReq {
@IsOptional() @IsOptional()
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
} }
export class AdminPostSalesChannelsChannelProductsBatchReq {
@IsArray()
product_ids: string[]
}