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:
Stevche Radevski
2024-04-01 09:33:52 +00:00
committed by GitHub
parent fe1d3a4a78
commit 585818eaf8
10 changed files with 658 additions and 694 deletions
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,
+1 -1
View File
@@ -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.
*/