feat: Add support for setting sales channel when creating a product (#6986)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user