feat(core-flows, product): options checks on product create/update (#9171)

**What**
- validate that variants are unique with respect to options on product update/create and variant update/create
- validate that the product has options upon creation
- ensure variants have the same number of option values as the product has options
- admin error handling
- update tests

---

FIXES FRMW-2707 CC-556
This commit is contained in:
Frane Polić
2024-10-15 09:06:51 +00:00
committed by GitHub
parent 20d19c1d67
commit 48cc00e991
21 changed files with 501 additions and 102 deletions
@@ -74,9 +74,11 @@ export const buildProductAndRelationsData = ({
{
title: faker.commerce.productName(),
sku: faker.commerce.productName(),
options: {
[defaultOptionTitle]: defaultOptionValue,
},
options: options
? { [options[0].title]: options[0].values[0] }
: {
[defaultOptionTitle]: defaultOptionValue,
},
},
],
// TODO: add categories, must be created first
@@ -4,6 +4,7 @@ import {
IProductModuleService,
ProductDTO,
ProductVariantDTO,
UpdateProductVariantDTO,
} from "@medusajs/framework/types"
import {
CommonEvents,
@@ -66,7 +67,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
id: "test-1",
title: "variant 1",
product_id: productOne.id,
options: { size: "large" },
options: { size: "large", color: "red" },
} as CreateProductVariantDTO)
variantTwo = await service.createProductVariants({
@@ -227,44 +228,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
)
})
it("should upsert the options of a variant successfully", async () => {
await service.upsertProductVariants([
{
id: variantOne.id,
options: { size: "small" },
},
])
const productVariant = await service.retrieveProductVariant(
variantOne.id,
{
relations: ["options"],
}
)
expect(productVariant.options).toEqual(
expect.arrayContaining([
expect.objectContaining({
value: "small",
}),
])
)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1)
expect(eventBusEmitSpy).toHaveBeenCalledWith(
[
composeMessage(ProductEvents.PRODUCT_VARIANT_UPDATED, {
data: { id: variantOne.id },
object: "product_variant",
source: Modules.PRODUCT,
action: CommonEvents.UPDATED,
}),
],
{
internal: true,
}
)
})
it("should do a partial update on the options of a variant successfully", async () => {
await service.updateProductVariants(variantOne.id, {
options: { size: "small", color: "red" },
@@ -311,7 +274,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
const data: CreateProductVariantDTO = {
title: "variant 3",
product_id: productOne.id,
options: { size: "small" },
options: { size: "small", color: "blue" },
}
const variant = await service.createProductVariants(data)
@@ -324,6 +287,9 @@ moduleIntegrationTestRunner<IProductModuleService>({
expect.objectContaining({
value: "small",
}),
expect.objectContaining({
value: "blue",
}),
]),
})
)
@@ -357,7 +323,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
},
{
title: "color",
values: ["red", "blue"],
values: ["red", "yellow"],
},
],
} as CreateProductDTO)
@@ -366,12 +332,12 @@ moduleIntegrationTestRunner<IProductModuleService>({
{
title: "new variant",
product_id: productOne.id,
options: { size: "small" },
options: { size: "small", color: "red" },
},
{
title: "new variant",
product_id: productThree.id,
options: { size: "small" },
options: { size: "small", color: "yellow" },
},
]
@@ -389,6 +355,12 @@ moduleIntegrationTestRunner<IProductModuleService>({
?.values?.find((v) => v.value === "small")?.id,
value: "small",
}),
expect.objectContaining({
id: productOne.options
.find((o) => o.title === "color")
?.values?.find((v) => v.value === "red")?.id,
value: "red",
}),
]),
}),
expect.objectContaining({
@@ -401,11 +373,125 @@ moduleIntegrationTestRunner<IProductModuleService>({
?.values?.find((v) => v.value === "small")?.id,
value: "small",
}),
expect.objectContaining({
id: productThree.options
.find((o) => o.title === "color")
?.values?.find((v) => v.value === "yellow")?.id,
value: "yellow",
}),
]),
}),
])
)
})
it("should throw if there is an existing variant with same options combination", async () => {
let error
const productFour = await service.createProducts({
id: "product-4",
title: "product 4",
status: ProductStatus.PUBLISHED,
options: [
{
title: "size",
values: ["large", "small"],
},
{
title: "color",
values: ["red", "blue"],
},
],
} as CreateProductDTO)
const data: CreateProductVariantDTO[] = [
{
title: "new variant",
product_id: productFour.id,
options: { size: "small", color: "red" },
},
]
const [variant] = await service.createProductVariants(data)
expect(variant).toEqual(
expect.objectContaining({
title: "new variant",
product_id: productFour.id,
options: expect.arrayContaining([
expect.objectContaining({
id: productFour.options
.find((o) => o.title === "size")
?.values?.find((v) => v.value === "small")?.id,
value: "small",
}),
expect.objectContaining({
id: productFour.options
.find((o) => o.title === "color")
?.values?.find((v) => v.value === "red")?.id,
value: "red",
}),
]),
})
)
try {
await service.createProductVariants([
{
title: "new variant",
product_id: productFour.id,
options: { size: "small", color: "red" },
},
] as CreateProductVariantDTO[])
} catch (e) {
error = e
}
expect(error.message).toEqual(
`Variant (${variant.title}) with provided options already exists.`
)
})
it("should throw if there is an existing variant with same options combination (on update)", async () => {
const productFour = await service.createProducts({
id: "product-4",
title: "product 4",
status: ProductStatus.PUBLISHED,
options: [
{
title: "size",
values: ["large", "small"],
},
{
title: "color",
values: ["red", "blue"],
},
],
variants: [
{
title: "new variant 1",
options: { size: "small", color: "red" },
},
{
title: "new variant 2",
options: { size: "small", color: "blue" },
},
],
} as CreateProductDTO)
const error = await service
.updateProductVariants(
productFour.variants.find((v) => v.title === "new variant 2")!.id,
{
options: { size: "small", color: "red" },
} as UpdateProductVariantDTO
)
.catch((err) => err)
expect(error.message).toEqual(
`Variant (new variant 1) with provided options already exists.`
)
})
})
describe("softDelete variant", () => {
@@ -127,10 +127,17 @@ moduleIntegrationTestRunner<IProductModuleService>({
id: "product-1",
title: "product 1",
status: ProductStatus.PUBLISHED,
options: [
{
title: "opt-title",
values: ["val-1", "val-2"],
},
],
variants: [
{
id: "variant-1",
title: "variant 1",
options: { "opt-title": "val-1" },
},
],
})
@@ -156,6 +163,10 @@ moduleIntegrationTestRunner<IProductModuleService>({
{
id: "variant-2",
title: "variant 2",
options: {
size: "large",
color: "blue",
},
},
{
id: "variant-3",
@@ -177,6 +188,12 @@ moduleIntegrationTestRunner<IProductModuleService>({
const data = buildProductAndRelationsData({
images,
thumbnail: images[0].url,
options: [
{
title: "opt-title",
values: ["val-1", "val-2"],
},
],
})
const variantTitle = data.variants[0].title
@@ -195,7 +212,10 @@ moduleIntegrationTestRunner<IProductModuleService>({
productBefore.title = "updated title"
productBefore.variants = [
...productBefore.variants!,
{
...productBefore.variants[0]!,
options: { "opt-title": "val-2" },
},
...data.variants,
]
productBefore.options = data.options
@@ -541,6 +561,34 @@ moduleIntegrationTestRunner<IProductModuleService>({
expect(error).toEqual(`Product with id: does-not-exist was not found`)
})
it("should throw because variant doesn't have all options set", async () => {
let error
try {
await service.createProducts([
{
title: "Product with variants and options",
options: [
{ title: "opt1", values: ["1", "2"] },
{ title: "opt2", values: ["3", "4"] },
],
variants: [
{
title: "missing option",
options: { opt1: "1" },
},
],
},
])
} catch (e) {
error = e
}
expect(error.message).toEqual(
`Product "Product with variants and options" has variants with missing options: [missing option]`
)
})
it("should update, create and delete variants", async () => {
const updateData = {
id: productTwo.id,
@@ -606,7 +654,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
)
})
it("should createa variant with id that was passed if it does not exist", async () => {
it("should create a variant with id that was passed if it does not exist", async () => {
const updateData = {
id: productTwo.id,
// Note: VariantThree is already assigned to productTwo, that should be deleted