feat: Add support for setting sales channel when creating a product (#6986)

This commit is contained in:
Stevche Radevski
2024-04-07 13:45:47 +02:00
committed by GitHub
parent d1728990e9
commit 44bcde92c8
13 changed files with 126 additions and 168 deletions

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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"

View File

@@ -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 },
})
}
)

View File

@@ -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<ProductTypes.CreateProductDTO, "variants"> & {
sales_channels?: { id: string }[]
variants?: (ProductTypes.CreateProductVariantDTO & {
prices?: PricingTypes.CreateMoneyAmountDTO[]
})[]
@@ -24,9 +26,10 @@ export const createProductsWorkflow = createWorkflow(
input: WorkflowData<WorkflowInput>
): WorkflowData<ProductTypes.ProductDTO[]> => {
// 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
}
)

View File

@@ -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<WorkflowInput>): WorkflowData<void> => {
removeVariantPricingLinkStep({ variant_ids: input.ids })
removeRemoteLinkStep({
[Modules.PRODUCT]: { variant_id: input.ids },
}).config({ name: "remove-variant-link-step" })
return deleteProductVariantsStep(input.ids)
}
)

View File

@@ -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)
}
)

View File

@@ -16,6 +16,7 @@ export const updateProductsWorkflow = createWorkflow(
input: WorkflowData<WorkflowInput>
): WorkflowData<ProductTypes.ProductDTO[]> => {
// TODO: Delete price sets for removed variants
// TODO Update sales channel links
return updateProductsStep(input)
}
)

View File

@@ -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 }) => {

View File

@@ -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)

View File

@@ -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<WorkflowInput>): WorkflowData<SalesChannelDTO[]> => {
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 })
}
)

View File

@@ -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<WorkflowInput>): WorkflowData<SalesChannelDTO[]> => {
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 })
}
)

View File

@@ -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[]