feat: Pricing update and refactor product API tests to not rely on internals (#6892)
This PR achieves the following - Remove dependency on internals for seeding the tests (more work left, but major work done) - Adds the workflow for updating variant's price I will do a follow-up PR to further clean up the tests and remove all internal dependencies
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,17 @@ import {
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
type UpdatePriceSetsStepInput = {
|
||||
selector: PricingTypes.FilterablePriceSetProps
|
||||
update: PricingTypes.UpdatePriceSetDTO
|
||||
selector?: PricingTypes.FilterablePriceSetProps
|
||||
update?: PricingTypes.UpdatePriceSetDTO
|
||||
}
|
||||
export const updatePriceSetsStepId = "update-price-sets"
|
||||
export const updatePriceSetsStep = createStep(
|
||||
updatePriceSetsStepId,
|
||||
async (data: UpdatePriceSetsStepInput, { container }) => {
|
||||
if (!data.selector || !data.update) {
|
||||
return new StepResponse([], null)
|
||||
}
|
||||
|
||||
const pricingModule = container.resolve<IPricingModuleService>(
|
||||
ModuleRegistrationName.PRICING
|
||||
)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
arrayDifference,
|
||||
} from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
type StepInput = {
|
||||
ids: string[]
|
||||
}
|
||||
|
||||
export const getVariantPricingLinkStepId = "get-variant-pricing-link"
|
||||
export const getVariantPricingLinkStep = createStep(
|
||||
getVariantPricingLinkStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
if (!data.ids.length) {
|
||||
return new StepResponse([])
|
||||
}
|
||||
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
|
||||
const linkService = remoteLink.getLinkModule(
|
||||
Modules.PRODUCT,
|
||||
"variant_id",
|
||||
Modules.PRICING,
|
||||
"price_set_id"
|
||||
)
|
||||
|
||||
const existingItems = await linkService.list(
|
||||
{ variant_id: data.ids },
|
||||
{ select: ["variant_id", "price_set_id"] }
|
||||
)
|
||||
|
||||
if (existingItems.length !== data.ids.length) {
|
||||
const missing = arrayDifference(
|
||||
data.ids,
|
||||
existingItems.map((i) => i.variant_id)
|
||||
)
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Variants with IDs ${missing.join(", ")} do not have prices associated.`
|
||||
)
|
||||
}
|
||||
|
||||
return new StepResponse(existingItems)
|
||||
}
|
||||
)
|
||||
@@ -34,27 +34,21 @@ export const createProductVariantsWorkflow = createWorkflow(
|
||||
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)
|
||||
const pricesToCreate = transform({ input, createdVariants }, (data) =>
|
||||
data.createdVariants.map((v, i) => {
|
||||
return {
|
||||
prices: data.input.product_variants[i]?.prices,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// 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 createdPriceSets = createPriceSetsStep(pricesToCreate)
|
||||
|
||||
const variantAndPriceSets = transform(
|
||||
{ variantsWithAssociatedPrices, createdPriceSets },
|
||||
{ createdVariants, createdPriceSets },
|
||||
(data) => {
|
||||
return data.variantsWithAssociatedPrices.map((variant, i) => ({
|
||||
return data.createdVariants.map((variant, i) => ({
|
||||
variant: variant,
|
||||
price_set: data.createdPriceSets[i],
|
||||
}))
|
||||
@@ -77,15 +71,12 @@ export const createProductVariantsWorkflow = createWorkflow(
|
||||
|
||||
return transform(
|
||||
{
|
||||
createdVariants,
|
||||
variantAndPriceSets,
|
||||
},
|
||||
(data) => {
|
||||
return data.createdVariants.map((variant) => ({
|
||||
...variant,
|
||||
price_set: data.variantAndPriceSets.find(
|
||||
(v) => v.variant.id === variant.id
|
||||
)?.price_set,
|
||||
return data.variantAndPriceSets.map((variantAndPriceSet) => ({
|
||||
...variantAndPriceSet.variant,
|
||||
...variantAndPriceSet.price_set,
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -44,16 +44,21 @@ export const createProductsWorkflow = createWorkflow(
|
||||
.map((p, i) => {
|
||||
const inputProduct = data.input.products[i]
|
||||
return p.variants?.map((v, j) => ({
|
||||
id: v.id,
|
||||
...v,
|
||||
prices: inputProduct?.variants?.[j]?.prices,
|
||||
}))
|
||||
})
|
||||
.flat()
|
||||
.filter((v) => !!v.prices?.length)
|
||||
}
|
||||
)
|
||||
|
||||
const createdPriceSets = createPriceSetsStep(variantsWithAssociatedPrices)
|
||||
const pricesToCreate = transform({ variantsWithAssociatedPrices }, (data) =>
|
||||
data.variantsWithAssociatedPrices.map((v) => ({
|
||||
prices: v.prices,
|
||||
}))
|
||||
)
|
||||
|
||||
const createdPriceSets = createPriceSetsStep(pricesToCreate)
|
||||
|
||||
const variantAndPriceSets = transform(
|
||||
{ variantsWithAssociatedPrices, createdPriceSets },
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { PricingTypes, ProductTypes } from "@medusajs/types"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { updateProductVariantsStep } from "../steps"
|
||||
import { updatePriceSetsStep } from "../../pricing"
|
||||
import { getVariantPricingLinkStep } from "../steps/get-variant-pricing-link"
|
||||
|
||||
type UpdateProductVariantsStepInput = {
|
||||
selector: ProductTypes.FilterableProductVariantProps
|
||||
update: ProductTypes.UpdateProductVariantDTO
|
||||
update: ProductTypes.UpdateProductVariantDTO & {
|
||||
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||
}
|
||||
}
|
||||
|
||||
type WorkflowInput = UpdateProductVariantsStepInput
|
||||
@@ -15,6 +23,72 @@ export const updateProductVariantsWorkflow = createWorkflow(
|
||||
(
|
||||
input: WorkflowData<WorkflowInput>
|
||||
): WorkflowData<ProductTypes.ProductVariantDTO[]> => {
|
||||
return updateProductVariantsStep(input)
|
||||
// Passing prices to the product module will fail, we want to keep them for after the variant is updated.
|
||||
const updateWithoutPrices = transform({ input }, (data) => {
|
||||
return {
|
||||
selector: data.input.selector,
|
||||
update: {
|
||||
...data.input.update,
|
||||
prices: undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const updatedVariants = updateProductVariantsStep(updateWithoutPrices)
|
||||
|
||||
// We don't want to do any pricing updates if the prices didn't change
|
||||
const variantIds = transform({ input, updatedVariants }, (data) => {
|
||||
if (!data.input.update.prices) {
|
||||
return []
|
||||
}
|
||||
|
||||
return data.updatedVariants.map((v) => v.id)
|
||||
})
|
||||
|
||||
const variantPriceSetLinks = getVariantPricingLinkStep({
|
||||
ids: variantIds,
|
||||
})
|
||||
|
||||
const pricesToUpdate = transform(
|
||||
{ input, variantPriceSetLinks },
|
||||
(data) => {
|
||||
if (!data.variantPriceSetLinks.length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
selector: {
|
||||
ids: data.variantPriceSetLinks.map((link) => link.price_set_id),
|
||||
} as PricingTypes.FilterablePriceSetProps,
|
||||
update: {
|
||||
prices: data.input.update.prices,
|
||||
} as PricingTypes.UpdatePriceSetDTO,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const updatedPriceSets = updatePriceSetsStep(pricesToUpdate)
|
||||
|
||||
// We want to correctly return the variants with their associated price sets and the prices coming from it
|
||||
return transform(
|
||||
{
|
||||
variantPriceSetLinks,
|
||||
updatedVariants,
|
||||
updatedPriceSets,
|
||||
},
|
||||
(data) => {
|
||||
return data.updatedVariants.map((variant, i) => {
|
||||
const linkForVariant = data.variantPriceSetLinks.find(
|
||||
(link) => link.variant_id === variant.id
|
||||
)
|
||||
|
||||
const priceSetForVariant = data.updatedPriceSets.find(
|
||||
(priceSet) => priceSet.id === linkForVariant?.price_set_id
|
||||
)
|
||||
|
||||
return { ...variant, price_set: priceSetForVariant }
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -47,6 +47,10 @@ export const remapProduct = (p: ProductDTO) => {
|
||||
}
|
||||
|
||||
export const remapVariant = (v: ProductVariantDTO) => {
|
||||
if (!v) {
|
||||
return v
|
||||
}
|
||||
|
||||
return {
|
||||
...v,
|
||||
prices: (v as any).price_set?.prices?.map((price) => ({
|
||||
|
||||
@@ -646,7 +646,8 @@ export class AdminPostProductsProductOptionsOptionReq {
|
||||
// eslint-disable-next-line max-len
|
||||
export class ProductVariantReq extends AdminPostProductsProductVariantsVariantReq {
|
||||
@IsString()
|
||||
id: string
|
||||
@IsOptional()
|
||||
id?: string
|
||||
}
|
||||
|
||||
export class ProductTagReq {
|
||||
|
||||
@@ -1108,7 +1108,7 @@ export default class ProductModuleService<
|
||||
const productData = await this.productService_.upsertWithReplace(
|
||||
normalizedInput,
|
||||
{
|
||||
relations: ["type", "collection", "images", "tags", "categories"],
|
||||
relations: ["images", "tags", "categories"],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
@@ -1164,7 +1164,7 @@ export default class ProductModuleService<
|
||||
const productData = await this.productService_.upsertWithReplace(
|
||||
normalizedInput,
|
||||
{
|
||||
relations: ["type", "collection", "images", "tags", "categories"],
|
||||
relations: ["images", "tags", "categories"],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
@@ -1350,6 +1350,10 @@ export default class ProductModuleService<
|
||||
}
|
||||
)
|
||||
|
||||
if (!variantOptions.length) {
|
||||
return variant
|
||||
}
|
||||
|
||||
return {
|
||||
...variant,
|
||||
options: variantOptions,
|
||||
|
||||
@@ -673,7 +673,7 @@ export interface ProductOptionValueDTO {
|
||||
* @prop handle - The handles to filter products by.
|
||||
* @prop id - The IDs to filter products by.
|
||||
* @prop tags - Filters on a product's tags.
|
||||
* @prop categories - Filters on a product's categories.
|
||||
* @prop category_id - Filters on a product's category_id.
|
||||
* @prop collection_id - Filters a product by its associated collections.
|
||||
*/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user