feat:Allow updating prices through product update workflow (#8316)
* feat:Allow updating prices through product update workflow * fix:Changes based on PR feedback
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
import { UpdateProductsStepInput } from "./update-products"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||
|
||||
export const getVariantIdsForProductsStepId = "get-variant-ids-for-products"
|
||||
export const getVariantIdsForProductsStep = createStep(
|
||||
getVariantIdsForProductsStepId,
|
||||
async (data: UpdateProductsStepInput, { container }) => {
|
||||
const service = container.resolve<IProductModuleService>(
|
||||
ModuleRegistrationName.PRODUCT
|
||||
)
|
||||
let filters = {}
|
||||
if ("products" in data) {
|
||||
if (!data.products.length) {
|
||||
return new StepResponse([])
|
||||
}
|
||||
|
||||
filters = {
|
||||
id: data.products.map((p) => p.id),
|
||||
}
|
||||
} else {
|
||||
filters = data.selector
|
||||
}
|
||||
|
||||
const products = await service.listProducts(filters, {
|
||||
select: ["variants.id"],
|
||||
relations: ["variants"],
|
||||
take: null,
|
||||
})
|
||||
|
||||
return new StepResponse(
|
||||
products.flatMap((p) => p.variants.map((v) => v.id))
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -38,11 +38,6 @@ export const groupProductsForBatchStep = createStep(
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Currently the update product workflow doesn't update variant pricing, but we should probably add support for it.
|
||||
product.variants?.forEach((variant: any) => {
|
||||
delete variant.prices
|
||||
})
|
||||
|
||||
acc.toUpdate.push(
|
||||
product as HttpTypes.AdminUpdateProduct & { id: string }
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ export const waitConfirmationProductImportStep = createStep(
|
||||
name: waitConfirmationProductImportStepId,
|
||||
async: true,
|
||||
// After an hour we want to timeout and cancel the import so we don't have orphaned workflows
|
||||
timeout: 60 * 60 * 1 * 1000,
|
||||
timeout: 60 * 60 * 1,
|
||||
},
|
||||
async () => {}
|
||||
)
|
||||
|
||||
@@ -22,3 +22,4 @@ export * from "./update-product-variants"
|
||||
export * from "./update-products"
|
||||
export * from "./export-products"
|
||||
export * from "./import-products"
|
||||
export * from "./upsert-variant-prices"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { updateProductsStep } from "../steps/update-products"
|
||||
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import {
|
||||
CreateMoneyAmountDTO,
|
||||
ProductTypes,
|
||||
UpdateProductVariantWorkflowInputDTO,
|
||||
} from "@medusajs/types"
|
||||
import { arrayDifference, Modules } from "@medusajs/utils"
|
||||
import {
|
||||
createWorkflow,
|
||||
@@ -12,17 +16,21 @@ import {
|
||||
dismissRemoteLinkStep,
|
||||
useRemoteQueryStep,
|
||||
} from "../../common"
|
||||
import { upsertVariantPricesWorkflow } from "./upsert-variant-prices"
|
||||
import { getVariantIdsForProductsStep } from "../steps/get-variant-ids-for-products"
|
||||
|
||||
type UpdateProductsStepInputSelector = {
|
||||
selector: ProductTypes.FilterableProductProps
|
||||
update: ProductTypes.UpdateProductDTO & {
|
||||
update: Omit<ProductTypes.UpdateProductDTO, "variants"> & {
|
||||
sales_channels?: { id: string }[]
|
||||
variants?: UpdateProductVariantWorkflowInputDTO[]
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateProductsStepInputProducts = {
|
||||
products: (ProductTypes.UpsertProductDTO & {
|
||||
products: (Omit<ProductTypes.UpsertProductDTO, "variants"> & {
|
||||
sales_channels?: { id: string }[]
|
||||
variants?: UpdateProductVariantWorkflowInputDTO[]
|
||||
})[]
|
||||
}
|
||||
|
||||
@@ -46,6 +54,10 @@ function prepareUpdateProductInput({
|
||||
products: input.products.map((p) => ({
|
||||
...p,
|
||||
sales_channels: undefined,
|
||||
variants: p.variants?.map((v) => ({
|
||||
...v,
|
||||
prices: undefined,
|
||||
})),
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -55,6 +67,10 @@ function prepareUpdateProductInput({
|
||||
update: {
|
||||
...input.update,
|
||||
sales_channels: undefined,
|
||||
variants: input.update?.variants?.map((v) => ({
|
||||
...v,
|
||||
prices: undefined,
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -120,19 +136,67 @@ function prepareSalesChannelLinks({
|
||||
return []
|
||||
}
|
||||
|
||||
function prepareToDeleteLinks({
|
||||
currentLinks,
|
||||
function prepareVariantPrices({
|
||||
input,
|
||||
updatedProducts,
|
||||
}: {
|
||||
currentLinks: {
|
||||
updatedProducts: ProductTypes.ProductDTO[]
|
||||
input: WorkflowInput
|
||||
}): {
|
||||
variant_id: string
|
||||
product_id: string
|
||||
prices?: CreateMoneyAmountDTO[]
|
||||
}[] {
|
||||
if ("products" in input) {
|
||||
if (!input.products.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Note: We rely on the ordering of input and update here.
|
||||
return input.products.flatMap((product, i) => {
|
||||
if (!product.variants?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const updatedProduct = updatedProducts[i]
|
||||
return product.variants.map((variant, j) => {
|
||||
const updatedVariant = updatedProduct.variants[j]
|
||||
|
||||
return {
|
||||
product_id: updatedProduct.id,
|
||||
variant_id: updatedVariant.id,
|
||||
prices: variant.prices,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (input.selector && input.update?.variants?.length) {
|
||||
return updatedProducts.flatMap((p) => {
|
||||
return input.update.variants!.map((variant, i) => ({
|
||||
product_id: p.id,
|
||||
variant_id: p.variants[i].id,
|
||||
prices: variant.prices,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function prepareToDeleteSalesChannelLinks({
|
||||
currentSalesChannelLinks,
|
||||
}: {
|
||||
currentSalesChannelLinks: {
|
||||
product_id: string
|
||||
sales_channel_id: string
|
||||
}[]
|
||||
}) {
|
||||
if (!currentLinks.length) {
|
||||
if (!currentSalesChannelLinks.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return currentLinks.map(({ product_id, sales_channel_id }) => ({
|
||||
return currentSalesChannelLinks.map(({ product_id, sales_channel_id }) => ({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id,
|
||||
},
|
||||
@@ -148,7 +212,7 @@ export const updateProductsWorkflow = createWorkflow(
|
||||
(
|
||||
input: WorkflowData<WorkflowInput>
|
||||
): WorkflowData<ProductTypes.ProductDTO[]> => {
|
||||
// TODO: Delete price sets for removed variants
|
||||
const previousVariantIds = getVariantIdsForProductsStep(input)
|
||||
|
||||
const toUpdateInput = transform({ input }, prepareUpdateProductInput)
|
||||
const updatedProducts = updateProductsStep(toUpdateInput)
|
||||
@@ -157,20 +221,32 @@ export const updateProductsWorkflow = createWorkflow(
|
||||
updateProductIds
|
||||
)
|
||||
|
||||
const currentLinks = useRemoteQueryStep({
|
||||
entry_point: "product_sales_channel",
|
||||
fields: ["product_id", "sales_channel_id"],
|
||||
variables: { filters: { product_id: updatedProductIds } },
|
||||
})
|
||||
|
||||
const toDeleteLinks = transform({ currentLinks }, prepareToDeleteLinks)
|
||||
|
||||
const salesChannelLinks = transform(
|
||||
{ input, updatedProducts },
|
||||
prepareSalesChannelLinks
|
||||
)
|
||||
|
||||
dismissRemoteLinkStep(toDeleteLinks)
|
||||
const variantPrices = transform(
|
||||
{ input, updatedProducts },
|
||||
prepareVariantPrices
|
||||
)
|
||||
|
||||
const currentSalesChannelLinks = useRemoteQueryStep({
|
||||
entry_point: "product_sales_channel",
|
||||
fields: ["product_id", "sales_channel_id"],
|
||||
variables: { filters: { product_id: updatedProductIds } },
|
||||
})
|
||||
|
||||
const toDeleteSalesChannelLinks = transform(
|
||||
{ currentSalesChannelLinks },
|
||||
prepareToDeleteSalesChannelLinks
|
||||
)
|
||||
|
||||
upsertVariantPricesWorkflow.runAsStep({
|
||||
input: { variantPrices, previousVariantIds },
|
||||
})
|
||||
|
||||
dismissRemoteLinkStep(toDeleteSalesChannelLinks)
|
||||
|
||||
createRemoteLinkStep(salesChannelLinks)
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
CreatePricesDTO,
|
||||
UpdatePricesDTO,
|
||||
CreatePriceSetDTO,
|
||||
} from "@medusajs/types"
|
||||
import { Modules, arrayDifference } from "@medusajs/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { removeRemoteLinkStep, useRemoteQueryStep } from "../../common"
|
||||
import { createPriceSetsStep, updatePriceSetsStep } from "../../pricing"
|
||||
import { createVariantPricingLinkStep } from "../steps"
|
||||
|
||||
type WorkflowInput = {
|
||||
variantPrices: {
|
||||
variant_id: string
|
||||
product_id: string
|
||||
prices?: (CreatePricesDTO | UpdatePricesDTO)[]
|
||||
}[]
|
||||
previousVariantIds: string[]
|
||||
}
|
||||
|
||||
export const upsertVariantPricesWorkflowId = "upsert-variant-prices"
|
||||
export const upsertVariantPricesWorkflow = createWorkflow(
|
||||
upsertVariantPricesWorkflowId,
|
||||
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
|
||||
const removedVariantIds = transform({ input }, (data) => {
|
||||
return arrayDifference(
|
||||
data.input.previousVariantIds,
|
||||
data.input.variantPrices.map((v) => v.variant_id)
|
||||
)
|
||||
})
|
||||
|
||||
removeRemoteLinkStep({
|
||||
[Modules.PRODUCT]: { variant_id: removedVariantIds },
|
||||
}).config({ name: "remove-variant-link-step" })
|
||||
|
||||
const { newVariants, existingVariants } = transform({ input }, (data) => {
|
||||
const previousMap = new Set(data.input.previousVariantIds.map((v) => v))
|
||||
|
||||
return {
|
||||
existingVariants: data.input.variantPrices.filter((v) =>
|
||||
previousMap.has(v.variant_id)
|
||||
),
|
||||
newVariants: data.input.variantPrices.filter(
|
||||
(v) => !previousMap.has(v.variant_id)
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
const existingVariantIds = transform({ existingVariants }, (data) =>
|
||||
data.existingVariants.map((v) => v.variant_id)
|
||||
)
|
||||
|
||||
const existingLinks = useRemoteQueryStep({
|
||||
entry_point: "product_variant_price_set",
|
||||
fields: ["variant_id", "price_set_id"],
|
||||
variables: { filters: { variant_id: existingVariantIds } },
|
||||
})
|
||||
|
||||
const pricesToUpdate = transform(
|
||||
{ existingVariants, existingLinks },
|
||||
(data) => {
|
||||
const linksMap = new Map(
|
||||
data.existingLinks.map((l) => [l.variant_id, l.price_set_id])
|
||||
)
|
||||
|
||||
return {
|
||||
price_sets: data.existingVariants
|
||||
.map((v) => {
|
||||
const priceSetId = linksMap.get(v.variant_id)
|
||||
|
||||
if (!priceSetId) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
id: priceSetId,
|
||||
prices: v.prices,
|
||||
}
|
||||
})
|
||||
.filter(Boolean),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
updatePriceSetsStep(pricesToUpdate)
|
||||
|
||||
// Note: We rely on the same order of input and output when creating variants here, make sure that assumption holds
|
||||
const pricesToCreate = transform({ newVariants }, (data) =>
|
||||
data.newVariants.map((v) => {
|
||||
return {
|
||||
prices: v.prices,
|
||||
} as CreatePriceSetDTO
|
||||
})
|
||||
)
|
||||
|
||||
const createdPriceSets = createPriceSetsStep(pricesToCreate)
|
||||
|
||||
const variantAndPriceSetLinks = transform(
|
||||
{ newVariants, createdPriceSets },
|
||||
(data) => {
|
||||
return {
|
||||
links: data.newVariants.map((variant, i) => ({
|
||||
variant_id: variant.variant_id,
|
||||
price_set_id: data.createdPriceSets[i].id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
createVariantPricingLinkStep(variantAndPriceSetLinks)
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user