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:
1 parent
fe1d3a4a78
commit
585818eaf8
10 files changed
+658
-694
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -7,13 +7,17 @@ import {
|
|||||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
type UpdatePriceSetsStepInput = {
|
type UpdatePriceSetsStepInput = {
|
||||||
selector: PricingTypes.FilterablePriceSetProps
|
selector?: PricingTypes.FilterablePriceSetProps
|
||||||
update: PricingTypes.UpdatePriceSetDTO
|
update?: PricingTypes.UpdatePriceSetDTO
|
||||||
}
|
}
|
||||||
export const updatePriceSetsStepId = "update-price-sets"
|
export const updatePriceSetsStepId = "update-price-sets"
|
||||||
export const updatePriceSetsStep = createStep(
|
export const updatePriceSetsStep = createStep(
|
||||||
updatePriceSetsStepId,
|
updatePriceSetsStepId,
|
||||||
async (data: UpdatePriceSetsStepInput, { container }) => {
|
async (data: UpdatePriceSetsStepInput, { container }) => {
|
||||||
|
if (!data.selector || !data.update) {
|
||||||
|
return new StepResponse([], null)
|
||||||
|
}
|
||||||
|
|
||||||
const pricingModule = container.resolve<IPricingModuleService>(
|
const pricingModule = container.resolve<IPricingModuleService>(
|
||||||
ModuleRegistrationName.PRICING
|
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)
|
const createdVariants = createProductVariantsStep(variantsWithoutPrices)
|
||||||
|
|
||||||
// Note: We rely on the same order of input and output when creating variants here, make sure that assumption holds
|
// Note: We rely on the same order of input and output when creating variants here, make sure that assumption holds
|
||||||
const variantsWithAssociatedPrices = transform(
|
const pricesToCreate = transform({ input, createdVariants }, (data) =>
|
||||||
{ input, createdVariants },
|
data.createdVariants.map((v, i) => {
|
||||||
(data) =>
|
return {
|
||||||
data.createdVariants
|
prices: data.input.product_variants[i]?.prices,
|
||||||
.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
|
// 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(
|
const variantAndPriceSets = transform(
|
||||||
{ variantsWithAssociatedPrices, createdPriceSets },
|
{ createdVariants, createdPriceSets },
|
||||||
(data) => {
|
(data) => {
|
||||||
return data.variantsWithAssociatedPrices.map((variant, i) => ({
|
return data.createdVariants.map((variant, i) => ({
|
||||||
variant: variant,
|
variant: variant,
|
||||||
price_set: data.createdPriceSets[i],
|
price_set: data.createdPriceSets[i],
|
||||||
}))
|
}))
|
||||||
@@ -77,15 +71,12 @@ export const createProductVariantsWorkflow = createWorkflow(
|
|||||||
|
|
||||||
return transform(
|
return transform(
|
||||||
{
|
{
|
||||||
createdVariants,
|
|
||||||
variantAndPriceSets,
|
variantAndPriceSets,
|
||||||
},
|
},
|
||||||
(data) => {
|
(data) => {
|
||||||
return data.createdVariants.map((variant) => ({
|
return data.variantAndPriceSets.map((variantAndPriceSet) => ({
|
||||||
...variant,
|
...variantAndPriceSet.variant,
|
||||||
price_set: data.variantAndPriceSets.find(
|
...variantAndPriceSet.price_set,
|
||||||
(v) => v.variant.id === variant.id
|
|
||||||
)?.price_set,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,16 +44,21 @@ export const createProductsWorkflow = createWorkflow(
|
|||||||
.map((p, i) => {
|
.map((p, i) => {
|
||||||
const inputProduct = data.input.products[i]
|
const inputProduct = data.input.products[i]
|
||||||
return p.variants?.map((v, j) => ({
|
return p.variants?.map((v, j) => ({
|
||||||
id: v.id,
|
...v,
|
||||||
prices: inputProduct?.variants?.[j]?.prices,
|
prices: inputProduct?.variants?.[j]?.prices,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.flat()
|
.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(
|
const variantAndPriceSets = transform(
|
||||||
{ variantsWithAssociatedPrices, createdPriceSets },
|
{ variantsWithAssociatedPrices, createdPriceSets },
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import { ProductTypes } from "@medusajs/types"
|
import { PricingTypes, ProductTypes } from "@medusajs/types"
|
||||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
import { updateProductVariantsStep } from "../steps"
|
import { updateProductVariantsStep } from "../steps"
|
||||||
|
import { updatePriceSetsStep } from "../../pricing"
|
||||||
|
import { getVariantPricingLinkStep } from "../steps/get-variant-pricing-link"
|
||||||
|
|
||||||
type UpdateProductVariantsStepInput = {
|
type UpdateProductVariantsStepInput = {
|
||||||
selector: ProductTypes.FilterableProductVariantProps
|
selector: ProductTypes.FilterableProductVariantProps
|
||||||
update: ProductTypes.UpdateProductVariantDTO
|
update: ProductTypes.UpdateProductVariantDTO & {
|
||||||
|
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowInput = UpdateProductVariantsStepInput
|
type WorkflowInput = UpdateProductVariantsStepInput
|
||||||
@@ -15,6 +23,72 @@ export const updateProductVariantsWorkflow = createWorkflow(
|
|||||||
(
|
(
|
||||||
input: WorkflowData<WorkflowInput>
|
input: WorkflowData<WorkflowInput>
|
||||||
): WorkflowData<ProductTypes.ProductVariantDTO[]> => {
|
): 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) => {
|
export const remapVariant = (v: ProductVariantDTO) => {
|
||||||
|
if (!v) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...v,
|
...v,
|
||||||
prices: (v as any).price_set?.prices?.map((price) => ({
|
prices: (v as any).price_set?.prices?.map((price) => ({
|
||||||
|
|||||||
@@ -646,7 +646,8 @@ export class AdminPostProductsProductOptionsOptionReq {
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
export class ProductVariantReq extends AdminPostProductsProductVariantsVariantReq {
|
export class ProductVariantReq extends AdminPostProductsProductVariantsVariantReq {
|
||||||
@IsString()
|
@IsString()
|
||||||
id: string
|
@IsOptional()
|
||||||
|
id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProductTagReq {
|
export class ProductTagReq {
|
||||||
|
|||||||
@@ -1108,7 +1108,7 @@ export default class ProductModuleService<
|
|||||||
const productData = await this.productService_.upsertWithReplace(
|
const productData = await this.productService_.upsertWithReplace(
|
||||||
normalizedInput,
|
normalizedInput,
|
||||||
{
|
{
|
||||||
relations: ["type", "collection", "images", "tags", "categories"],
|
relations: ["images", "tags", "categories"],
|
||||||
},
|
},
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
@@ -1164,7 +1164,7 @@ export default class ProductModuleService<
|
|||||||
const productData = await this.productService_.upsertWithReplace(
|
const productData = await this.productService_.upsertWithReplace(
|
||||||
normalizedInput,
|
normalizedInput,
|
||||||
{
|
{
|
||||||
relations: ["type", "collection", "images", "tags", "categories"],
|
relations: ["images", "tags", "categories"],
|
||||||
},
|
},
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
@@ -1350,6 +1350,10 @@ export default class ProductModuleService<
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!variantOptions.length) {
|
||||||
|
return variant
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...variant,
|
...variant,
|
||||||
options: variantOptions,
|
options: variantOptions,
|
||||||
|
|||||||
@@ -673,7 +673,7 @@ export interface ProductOptionValueDTO {
|
|||||||
* @prop handle - The handles to filter products by.
|
* @prop handle - The handles to filter products by.
|
||||||
* @prop id - The IDs to filter products by.
|
* @prop id - The IDs to filter products by.
|
||||||
* @prop tags - Filters on a product's tags.
|
* @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.
|
* @prop collection_id - Filters a product by its associated collections.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user