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:
Stevche Radevski
2024-07-29 19:56:03 +03:00
committed by GitHub
parent ed67d44d28
commit e98012a858
9 changed files with 382 additions and 27 deletions
@@ -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)
}
)