feat: Add product and pricing link on create and delete operations (#6740)
Things that remain to be done: 1. Handle product and variant updates 2. Add tests for the workflows independently 3. Align the endpoints to the new code conventions we defined 4. Finish up the update/upsert endpoints for variants All of those can be done in a separate PR, as this is quite large already.
This commit is contained in:
@@ -26,8 +26,8 @@ export const getVariantPriceSetsStep = createStep(
|
||||
{
|
||||
variant: {
|
||||
fields: ["id"],
|
||||
price: {
|
||||
fields: ["price_set_id"],
|
||||
price_set: {
|
||||
fields: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -42,8 +42,8 @@ export const getVariantPriceSetsStep = createStep(
|
||||
const priceSetIds: string[] = []
|
||||
|
||||
variantPriceSets.forEach((v) => {
|
||||
if (v.price?.price_set_id) {
|
||||
priceSetIds.push(v.price.price_set_id)
|
||||
if (v.price_set?.id) {
|
||||
priceSetIds.push(v.price_set.id)
|
||||
} else {
|
||||
notFound.push(v.id)
|
||||
}
|
||||
@@ -66,8 +66,8 @@ export const getVariantPriceSetsStep = createStep(
|
||||
)
|
||||
|
||||
const variantToCalculatedPriceSets = variantPriceSets.reduce(
|
||||
(acc, { id, price }) => {
|
||||
const calculatedPriceSet = idToPriceSet.get(price?.price_set_id)
|
||||
(acc, { id, price_set }) => {
|
||||
const calculatedPriceSet = idToPriceSet.get(price_set?.id)
|
||||
if (calculatedPriceSet) {
|
||||
acc[id] = calculatedPriceSet
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { CreatePriceSetDTO, IPricingModuleService } from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
export const createPriceSetsStepId = "create-price-sets"
|
||||
export const createPriceSetsStep = createStep(
|
||||
createPriceSetsStepId,
|
||||
async (data: CreatePriceSetDTO[], { container }) => {
|
||||
const pricingModule = container.resolve<IPricingModuleService>(
|
||||
ModuleRegistrationName.PRICING
|
||||
)
|
||||
|
||||
const priceSets = await pricingModule.create(data)
|
||||
|
||||
return new StepResponse(
|
||||
priceSets,
|
||||
priceSets.map((priceSet) => priceSet.id)
|
||||
)
|
||||
},
|
||||
async (priceSets, { container }) => {
|
||||
if (!priceSets?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const pricingModule = container.resolve<IPricingModuleService>(
|
||||
ModuleRegistrationName.PRICING
|
||||
)
|
||||
|
||||
await pricingModule.delete(priceSets)
|
||||
}
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from "./create-price-sets"
|
||||
export * from "./update-price-sets"
|
||||
export * from "./create-pricing-rule-types"
|
||||
export * from "./delete-pricing-rule-types"
|
||||
export * from "./update-pricing-rule-types"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IPricingModuleService, UpdatePriceSetDTO } from "@medusajs/types"
|
||||
import {
|
||||
convertItemResponseToUpdateRequest,
|
||||
getSelectsAndRelationsFromObjectArray,
|
||||
} from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
export const updatePriceSetsStepId = "update-price-sets"
|
||||
export const updatePriceSetsStep = createStep(
|
||||
updatePriceSetsStepId,
|
||||
async (data: UpdatePriceSetDTO[], { container }) => {
|
||||
const pricingModule = container.resolve<IPricingModuleService>(
|
||||
ModuleRegistrationName.PRICING
|
||||
)
|
||||
|
||||
const { selects, relations } = getSelectsAndRelationsFromObjectArray(data)
|
||||
const dataBeforeUpdate = await pricingModule.list(
|
||||
{ id: data.map((d) => d.id) },
|
||||
{ relations, select: selects }
|
||||
)
|
||||
|
||||
const updatedPriceSets = await pricingModule.update(data)
|
||||
|
||||
return new StepResponse(updatedPriceSets, {
|
||||
dataBeforeUpdate,
|
||||
selects,
|
||||
relations,
|
||||
})
|
||||
},
|
||||
async (revertInput, { container }) => {
|
||||
if (!revertInput) {
|
||||
return
|
||||
}
|
||||
|
||||
const { dataBeforeUpdate = [], selects, relations } = revertInput
|
||||
|
||||
const pricingModule = container.resolve<IPricingModuleService>(
|
||||
ModuleRegistrationName.PRICING
|
||||
)
|
||||
|
||||
await pricingModule.update(
|
||||
dataBeforeUpdate.map((data) =>
|
||||
convertItemResponseToUpdateRequest(data, selects, relations)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -9,7 +9,6 @@ export const createProductVariantsStep = createStep(
|
||||
const service = container.resolve<IProductModuleService>(
|
||||
ModuleRegistrationName.PRODUCT
|
||||
)
|
||||
|
||||
const created = await service.createVariants(data)
|
||||
return new StepResponse(
|
||||
created,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
type StepInput = {
|
||||
links: {
|
||||
variant_id: string
|
||||
price_set_id: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export const createVariantPricingLinkStepId = "create-variant-pricing-link"
|
||||
export const createVariantPricingLinkStep = createStep(
|
||||
createVariantPricingLinkStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
await remoteLink.create(
|
||||
data.links.map((entry) => ({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: entry.variant_id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: entry.price_set_id,
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
return new StepResponse(void 0, data)
|
||||
},
|
||||
async (data, { container }) => {
|
||||
if (!data?.links?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
const links = data.links.map((entry) => ({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: entry.variant_id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: entry.price_set_id,
|
||||
},
|
||||
}))
|
||||
|
||||
await remoteLink.dismiss(links)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
type StepInput = {
|
||||
ids: string[]
|
||||
}
|
||||
|
||||
export const getProductsStepId = "get-products"
|
||||
export const getProductsStep = createStep(
|
||||
getProductsStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const service = container.resolve<IProductModuleService>(
|
||||
ModuleRegistrationName.PRODUCT
|
||||
)
|
||||
|
||||
const products = await service.list(
|
||||
{ id: data.ids },
|
||||
{ relations: ["variants"], take: null }
|
||||
)
|
||||
return new StepResponse(products, products)
|
||||
}
|
||||
)
|
||||
@@ -1,6 +1,9 @@
|
||||
export * from "./create-products"
|
||||
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"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ILinkModule } from "@medusajs/types"
|
||||
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 }) => {
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
const linkModule: ILinkModule = remoteLink.getLinkModule(
|
||||
Modules.PRODUCT,
|
||||
"variant_id",
|
||||
Modules.PRICING,
|
||||
"price_set_id"
|
||||
)
|
||||
|
||||
const links = (await linkModule.list(
|
||||
{
|
||||
variant_id: data.variant_ids,
|
||||
},
|
||||
{ select: ["id", "variant_id", "price_set_id"] }
|
||||
)) as { id: string; variant_id: string; price_set_id: string }[]
|
||||
|
||||
await remoteLink.delete(links.map((link) => link.id))
|
||||
return new StepResponse(void 0, links)
|
||||
},
|
||||
async (prevData, { container }) => {
|
||||
if (!prevData?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
await remoteLink.create(
|
||||
prevData.map((entry) => ({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: entry.variant_id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: entry.price_set_id,
|
||||
},
|
||||
}))
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -1,9 +1,20 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { createProductVariantsStep } from "../steps"
|
||||
import { ProductTypes, PricingTypes } from "@medusajs/types"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
createProductVariantsStep,
|
||||
createVariantPricingLinkStep,
|
||||
} from "../steps"
|
||||
import { createPriceSetsStep } from "../../pricing"
|
||||
|
||||
// TODO: Create separate typings for the workflow input
|
||||
type WorkflowInput = {
|
||||
product_variants: ProductTypes.CreateProductVariantDTO[]
|
||||
product_variants: (ProductTypes.CreateProductVariantDTO & {
|
||||
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||
})[]
|
||||
}
|
||||
|
||||
export const createProductVariantsWorkflowId = "create-product-variants"
|
||||
@@ -12,6 +23,71 @@ export const createProductVariantsWorkflow = createWorkflow(
|
||||
(
|
||||
input: WorkflowData<WorkflowInput>
|
||||
): WorkflowData<ProductTypes.ProductVariantDTO[]> => {
|
||||
return createProductVariantsStep(input.product_variants)
|
||||
// Passing prices to the product module will fail, we want to keep them for after the variant is created.
|
||||
const variantsWithoutPrices = transform({ input }, (data) =>
|
||||
data.input.product_variants.map((v) => ({
|
||||
...v,
|
||||
prices: undefined,
|
||||
}))
|
||||
)
|
||||
|
||||
const createdVariants = createProductVariantsStep(variantsWithoutPrices)
|
||||
|
||||
// Note: We rely on the same order of input and output when creating variants here, make sure that assumption holds
|
||||
const variantsWithAssociatedPrices = transform(
|
||||
{ input, createdVariants },
|
||||
(data) =>
|
||||
data.createdVariants
|
||||
.map((variant, i) => {
|
||||
return {
|
||||
id: variant.id,
|
||||
prices: data.input.product_variants[i]?.prices,
|
||||
}
|
||||
})
|
||||
.flat()
|
||||
.filter((v) => !!v.prices?.length)
|
||||
)
|
||||
|
||||
// TODO: From here until the final transform the code is the same as when creating a product, we can probably refactor
|
||||
const createdPriceSets = createPriceSetsStep(variantsWithAssociatedPrices)
|
||||
|
||||
const variantAndPriceSets = transform(
|
||||
{ variantsWithAssociatedPrices, createdPriceSets },
|
||||
(data) => {
|
||||
return data.variantsWithAssociatedPrices.map((variant, i) => ({
|
||||
variant: variant,
|
||||
price_set: data.createdPriceSets[i],
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
const variantAndPriceSetLinks = transform(
|
||||
{ variantAndPriceSets },
|
||||
(data) => {
|
||||
return {
|
||||
links: data.variantAndPriceSets.map((entry) => ({
|
||||
variant_id: entry.variant.id,
|
||||
price_set_id: entry.price_set.id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
createVariantPricingLinkStep(variantAndPriceSetLinks)
|
||||
|
||||
return transform(
|
||||
{
|
||||
createdVariants,
|
||||
variantAndPriceSets,
|
||||
},
|
||||
(data) => {
|
||||
return data.createdVariants.map((variant) => ({
|
||||
...variant,
|
||||
price_set: data.variantAndPriceSets.find(
|
||||
(v) => v.variant.id === variant.id
|
||||
)?.price_set,
|
||||
}))
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { createProductsStep } from "../steps"
|
||||
import { ProductTypes, PricingTypes } from "@medusajs/types"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { createProductsStep, createVariantPricingLinkStep } from "../steps"
|
||||
import { createPriceSetsStep } from "../../pricing"
|
||||
|
||||
type WorkflowInput = { products: ProductTypes.CreateProductDTO[] }
|
||||
// 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"> & {
|
||||
variants?: (ProductTypes.CreateProductVariantDTO & {
|
||||
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||
})[]
|
||||
})[]
|
||||
}
|
||||
|
||||
export const createProductsWorkflowId = "create-products"
|
||||
export const createProductsWorkflow = createWorkflow(
|
||||
@@ -10,6 +23,78 @@ export const createProductsWorkflow = createWorkflow(
|
||||
(
|
||||
input: WorkflowData<WorkflowInput>
|
||||
): WorkflowData<ProductTypes.ProductDTO[]> => {
|
||||
return createProductsStep(input.products)
|
||||
// Passing prices to the product module will fail, we want to keep them for after the product is created.
|
||||
const productWithoutPrices = transform({ input }, (data) =>
|
||||
data.input.products.map((p) => ({
|
||||
...p,
|
||||
variants: p.variants?.map((v) => ({
|
||||
...v,
|
||||
prices: undefined,
|
||||
})),
|
||||
}))
|
||||
)
|
||||
|
||||
const createdProducts = createProductsStep(productWithoutPrices)
|
||||
|
||||
// Note: We rely on the same order of input and output when creating products here, make sure that assumption holds
|
||||
const variantsWithAssociatedPrices = transform(
|
||||
{ input, createdProducts },
|
||||
(data) => {
|
||||
return data.createdProducts
|
||||
.map((p, i) => {
|
||||
const inputProduct = data.input.products[i]
|
||||
return p.variants?.map((v, j) => ({
|
||||
id: v.id,
|
||||
prices: inputProduct?.variants?.[j]?.prices,
|
||||
}))
|
||||
})
|
||||
.flat()
|
||||
.filter((v) => !!v.prices?.length)
|
||||
}
|
||||
)
|
||||
|
||||
const createdPriceSets = createPriceSetsStep(variantsWithAssociatedPrices)
|
||||
|
||||
const variantAndPriceSets = transform(
|
||||
{ variantsWithAssociatedPrices, createdPriceSets },
|
||||
(data) =>
|
||||
data.variantsWithAssociatedPrices.map((variant, i) => ({
|
||||
variant: variant,
|
||||
price_set: data.createdPriceSets[i],
|
||||
}))
|
||||
)
|
||||
|
||||
const variantAndPriceSetLinks = transform(
|
||||
{ variantAndPriceSets },
|
||||
(data) => {
|
||||
return {
|
||||
links: data.variantAndPriceSets.map((entry) => ({
|
||||
variant_id: entry.variant.id,
|
||||
price_set_id: entry.price_set.id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
})),
|
||||
}))
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { deleteProductVariantsStep } from "../steps"
|
||||
import {
|
||||
deleteProductVariantsStep,
|
||||
removeVariantPricingLinkStep,
|
||||
} from "../steps"
|
||||
|
||||
type WorkflowInput = { ids: string[] }
|
||||
|
||||
@@ -7,6 +10,8 @@ export const deleteProductVariantsWorkflowId = "delete-product-variants"
|
||||
export const deleteProductVariantsWorkflow = createWorkflow(
|
||||
deleteProductVariantsWorkflowId,
|
||||
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
|
||||
// Question: Should we also remove the price set manually, or would that be cascaded?
|
||||
removeVariantPricingLinkStep({ variant_ids: input.ids })
|
||||
return deleteProductVariantsStep(input.ids)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { deleteProductsStep } from "../steps"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
deleteProductsStep,
|
||||
getProductsStep,
|
||||
removeVariantPricingLinkStep,
|
||||
} from "../steps"
|
||||
|
||||
type WorkflowInput = { ids: string[] }
|
||||
|
||||
@@ -7,6 +15,17 @@ export const deleteProductsWorkflowId = "delete-products"
|
||||
export const deleteProductsWorkflow = createWorkflow(
|
||||
deleteProductsWorkflowId,
|
||||
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
|
||||
const productsToDelete = getProductsStep({ ids: input.ids })
|
||||
const variantsToBeDeleted = transform({ productsToDelete }, (data) => {
|
||||
return data.productsToDelete
|
||||
.flatMap((product) => product.variants)
|
||||
.map((variant) => variant.id)
|
||||
})
|
||||
|
||||
// Question: Should we also remove the price set manually, or would that be cascaded?
|
||||
// Question: Since we soft-delete the product, how do we restore the product with the prices and the links?
|
||||
removeVariantPricingLinkStep({ variant_ids: variantsToBeDeleted })
|
||||
|
||||
return deleteProductsStep(input.ids)
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user