fix(core-flows): set SalesChannels on Product update (#7272)

This commit is contained in:
Frane Polić
2024-05-21 21:48:34 +02:00
committed by GitHub
parent e443a7be3f
commit b7df447682
5 changed files with 275 additions and 12 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/core-flows": patch
---
fix(core-flows): set SalesChannels for Products on update

View File

@@ -1749,6 +1749,123 @@ medusaIntegrationTestRunner({
)
})
it("updates products sales channels", async () => {
const [productId, salesChannel1Id, salesChannel2Id] = await breaking(
async () => {
const salesChannel1 = await simpleSalesChannelFactory(
dbConnection,
{
name: "test name 1",
description: "test description",
products: [baseProduct],
}
)
const salesChannel2 = await simpleSalesChannelFactory(
dbConnection,
{
name: "test name 2",
description: "test description",
salesChannel1,
// no assigned products
}
)
return [baseProduct.id, salesChannel1.id, salesChannel2.id]
},
async () => {
const salesChannelService = getContainer().resolve(
ModuleRegistrationName.SALES_CHANNEL
)
const salesChannel1 = await salesChannelService.create({
name: "test name 1",
description: "test description",
})
const salesChannel2 = await salesChannelService.create({
name: "test name 2",
description: "test description",
})
const newProduct = (
await api.post(
"/admin/products",
getProductFixture({
title: "Test saleschannel",
sales_channels: [{ id: salesChannel1.id }],
}),
adminHeaders
)
).data.product
return [newProduct.id, salesChannel1.id, salesChannel2.id]
}
)
await api.post(
`/admin/products/${productId}`,
{
title: "new name",
sales_channels: [
{ id: salesChannel1Id },
{ id: salesChannel2Id },
],
},
adminHeaders
)
let res = await api.get(
`/admin/products/${productId}?fields=*sales_channels`,
adminHeaders
)
expect(res.status).toEqual(200)
expect(res.data.product).toEqual(
expect.objectContaining({
id: productId,
title: "new name",
sales_channels: expect.arrayContaining([
expect.objectContaining({
id: salesChannel1Id,
name: "test name 1",
}),
expect.objectContaining({
id: salesChannel2Id,
name: "test name 2",
}),
]),
})
)
await api.post(
`/admin/products/${productId}`,
{
title: "new name 2",
sales_channels: [{ id: salesChannel2Id }], // update channels again to remove the first one
},
adminHeaders
)
res = await api.get(
`/admin/products/${productId}?fields=*sales_channels`,
adminHeaders
)
expect(res.status).toEqual(200)
expect(res.data.product).toEqual(
expect.objectContaining({
id: productId,
title: "new name 2",
sales_channels: expect.arrayContaining([
expect.objectContaining({
id: salesChannel2Id,
name: "test name 2",
}),
]),
})
)
})
it("fails to update product with invalid status", async () => {
const payload = {
status: null,

View File

@@ -6,7 +6,7 @@ import {
} from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type UpdateProductsStepInput =
export type UpdateProductsStepInput =
| {
selector: ProductTypes.FilterableProductProps
update: ProductTypes.UpdateProductDTO

View File

@@ -1,18 +1,135 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import {
createWorkflow,
transform,
WorkflowData,
} from "@medusajs/workflows-sdk"
import { updateProductsStep } from "../steps/update-products"
import {
createLinkStep,
removeRemoteLinkStep,
useRemoteQueryStep,
} from "../../common"
import { arrayDifference } from "@medusajs/utils"
import { DeleteEntityInput, Modules } from "@medusajs/modules-sdk"
type UpdateProductsStepInputSelector = {
selector: ProductTypes.FilterableProductProps
update: ProductTypes.UpdateProductDTO & {
sales_channels?: { id: string }[]
}
}
type UpdateProductsStepInputProducts = {
products: (ProductTypes.UpsertProductDTO & {
sales_channels?: { id: string }[]
})[]
}
type UpdateProductsStepInput =
| {
selector: ProductTypes.FilterableProductProps
update: ProductTypes.UpdateProductDTO
}
| {
products: ProductTypes.UpsertProductDTO[]
}
| UpdateProductsStepInputSelector
| UpdateProductsStepInputProducts
type WorkflowInput = UpdateProductsStepInput
function prepareUpdateProductInput({
input,
}: {
input: WorkflowInput
}): UpdateProductsStepInput {
if ("products" in input) {
return {
products: input.products.map((p) => ({
...p,
sales_channels: undefined,
})),
}
}
return {
selector: input.selector,
update: {
...input.update,
sales_channels: undefined,
},
}
}
function updateProductIds({
updatedProducts,
input,
}: {
updatedProducts: ProductTypes.ProductDTO[]
input: WorkflowInput
}) {
if ("products" in input) {
let productIds = updatedProducts.map((p) => p.id)
const discardedProductIds: string[] = input.products
.filter((p) => !p.sales_channels)
.map((p) => p.id as string)
return arrayDifference(productIds, discardedProductIds)
}
return !input.update.sales_channels ? [] : undefined
}
function prepareSalesChannelLinks({
input,
updatedProducts,
}: {
updatedProducts: ProductTypes.ProductDTO[]
input: WorkflowInput
}): Record<string, Record<string, any>>[] {
if ("products" in input) {
return input.products
.filter((p) => p.sales_channels)
.flatMap((p) =>
p.sales_channels!.map((sc) => ({
[Modules.PRODUCT]: {
product_id: p.id,
},
[Modules.SALES_CHANNEL]: {
sales_channel_id: sc.id,
},
}))
)
}
if (input.selector && input.update.sales_channels?.length) {
return updatedProducts.flatMap((p) =>
input.update.sales_channels!.map((channel) => ({
[Modules.PRODUCT]: {
product_id: p.id,
},
[Modules.SALES_CHANNEL]: {
sales_channel_id: channel.id,
},
}))
)
}
return []
}
function prepareToDeleteLinks({
currentLinks,
}: {
currentLinks: {
product_id: string
sales_channel_id: string
}[]
}) {
return currentLinks.map(({ product_id, sales_channel_id }) => ({
[Modules.PRODUCT]: {
product_id,
},
[Modules.SALES_CHANNEL]: {
sales_channel_id,
},
}))
}
export const updateProductsWorkflowId = "update-products"
export const updateProductsWorkflow = createWorkflow(
updateProductsWorkflowId,
@@ -20,7 +137,31 @@ 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)
const toUpdateInput = transform({ input }, prepareUpdateProductInput)
const updatedProducts = updateProductsStep(toUpdateInput)
const updatedProductIds = transform(
{ updatedProducts, input },
updateProductIds
)
const currentLinks = useRemoteQueryStep({
entry_point: "product_sales_channel",
fields: ["product_id", "sales_channel_id"],
variables: { product_id: updatedProductIds },
})
const toDeleteLinks = transform({ currentLinks }, prepareToDeleteLinks)
removeRemoteLinkStep(toDeleteLinks as DeleteEntityInput[])
const salesChannelLinks = transform(
{ input, updatedProducts },
prepareSalesChannelLinks
)
createLinkStep(salesChannelLinks)
return updatedProducts
}
)

View File

@@ -1,6 +1,6 @@
import { Modules } from "@medusajs/modules-sdk"
import { ContainerRegistrationKeys } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
interface StepInput {
links: {