fix(core-flows): set SalesChannels on Product update (#7272)
This commit is contained in:
1 parent
e443a7be3f
commit
b7df447682
5 files changed
+273
-10
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(core-flows): set SalesChannels for Products on update
|
||||||
@@ -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 () => {
|
it("fails to update product with invalid status", async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
status: null,
|
status: null,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
type UpdateProductsStepInput =
|
export type UpdateProductsStepInput =
|
||||||
| {
|
| {
|
||||||
selector: ProductTypes.FilterableProductProps
|
selector: ProductTypes.FilterableProductProps
|
||||||
update: ProductTypes.UpdateProductDTO
|
update: ProductTypes.UpdateProductDTO
|
||||||
|
|||||||
@@ -1,18 +1,135 @@
|
|||||||
import { ProductTypes } from "@medusajs/types"
|
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 { updateProductsStep } from "../steps/update-products"
|
||||||
|
|
||||||
type UpdateProductsStepInput =
|
import {
|
||||||
| {
|
createLinkStep,
|
||||||
|
removeRemoteLinkStep,
|
||||||
|
useRemoteQueryStep,
|
||||||
|
} from "../../common"
|
||||||
|
import { arrayDifference } from "@medusajs/utils"
|
||||||
|
import { DeleteEntityInput, Modules } from "@medusajs/modules-sdk"
|
||||||
|
|
||||||
|
type UpdateProductsStepInputSelector = {
|
||||||
selector: ProductTypes.FilterableProductProps
|
selector: ProductTypes.FilterableProductProps
|
||||||
update: ProductTypes.UpdateProductDTO
|
update: ProductTypes.UpdateProductDTO & {
|
||||||
|
sales_channels?: { id: string }[]
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
products: ProductTypes.UpsertProductDTO[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateProductsStepInputProducts = {
|
||||||
|
products: (ProductTypes.UpsertProductDTO & {
|
||||||
|
sales_channels?: { id: string }[]
|
||||||
|
})[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProductsStepInput =
|
||||||
|
| UpdateProductsStepInputSelector
|
||||||
|
| UpdateProductsStepInputProducts
|
||||||
|
|
||||||
type WorkflowInput = UpdateProductsStepInput
|
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 updateProductsWorkflowId = "update-products"
|
||||||
export const updateProductsWorkflow = createWorkflow(
|
export const updateProductsWorkflow = createWorkflow(
|
||||||
updateProductsWorkflowId,
|
updateProductsWorkflowId,
|
||||||
@@ -20,7 +137,31 @@ export const updateProductsWorkflow = createWorkflow(
|
|||||||
input: WorkflowData<WorkflowInput>
|
input: WorkflowData<WorkflowInput>
|
||||||
): WorkflowData<ProductTypes.ProductDTO[]> => {
|
): WorkflowData<ProductTypes.ProductDTO[]> => {
|
||||||
// TODO: Delete price sets for removed variants
|
// 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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
interface StepInput {
|
interface StepInput {
|
||||||
links: {
|
links: {
|
||||||
|
|||||||
Reference in new issue
Block a user