fix(promotion): eval conditions for rules are made accurate (#10915)

what:

- fixes eval conditions for promotion rules

RESOLVES CMRC-851
This commit is contained in:
Riqwan Thamir
2025-01-21 22:26:20 +01:00
committed by GitHub
parent cc73802ab3
commit 8119d9964b
6 changed files with 321 additions and 86 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/promotion": patch
---
fix(promotion): eval conditions for rules are corrected

View File

@@ -0,0 +1,65 @@
import { ProductStatus } from "@medusajs/utils"
export const medusaTshirtProduct = {
title: "Medusa T-Shirt",
handle: "t-shirt",
status: ProductStatus.PUBLISHED,
options: [
{
title: "Size",
values: ["S"],
},
{
title: "Color",
values: ["Black", "White"],
},
],
variants: [
{
title: "S / Black",
sku: "SHIRT-S-BLACK",
options: {
Size: "S",
Color: "Black",
},
manage_inventory: false,
prices: [
{
amount: 1500,
currency_code: "usd",
},
{
amount: 1500,
currency_code: "eur",
},
{
amount: 1300,
currency_code: "dkk",
},
],
},
{
title: "S / White",
sku: "SHIRT-S-WHITE",
options: {
Size: "S",
Color: "White",
},
manage_inventory: false,
prices: [
{
amount: 1500,
currency_code: "usd",
},
{
amount: 1500,
currency_code: "eur",
},
{
amount: 1300,
currency_code: "dkk",
},
],
},
],
}

View File

@@ -3,7 +3,6 @@ import {
Modules,
PriceListStatus,
PriceListType,
ProductStatus,
PromotionRuleOperator,
PromotionStatus,
PromotionType,
@@ -15,6 +14,7 @@ import {
} from "../../../../helpers/create-admin-user"
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
import { createAuthenticatedCustomer } from "../../../../modules/helpers/create-authenticated-customer"
import { medusaTshirtProduct } from "../../../__fixtures__/product"
jest.setTimeout(100000)
@@ -30,70 +30,6 @@ const shippingAddressData = {
postal_code: "94016",
}
const productData = {
title: "Medusa T-Shirt",
handle: "t-shirt",
status: ProductStatus.PUBLISHED,
options: [
{
title: "Size",
values: ["S"],
},
{
title: "Color",
values: ["Black", "White"],
},
],
variants: [
{
title: "S / Black",
sku: "SHIRT-S-BLACK",
options: {
Size: "S",
Color: "Black",
},
manage_inventory: false,
prices: [
{
amount: 1500,
currency_code: "usd",
},
{
amount: 1500,
currency_code: "eur",
},
{
amount: 1300,
currency_code: "dkk",
},
],
},
{
title: "S / White",
sku: "SHIRT-S-WHITE",
options: {
Size: "S",
Color: "White",
},
manage_inventory: false,
prices: [
{
amount: 1500,
currency_code: "usd",
},
{
amount: 1500,
currency_code: "eur",
},
{
amount: 1300,
currency_code: "dkk",
},
],
},
],
}
medusaIntegrationTestRunner({
env,
testSuite: ({ dbConnection, getContainer, api }) => {
@@ -150,8 +86,9 @@ medusaIntegrationTestRunner({
)
).data.region
product = (await api.post("/admin/products", productData, adminHeaders))
.data.product
product = (
await api.post("/admin/products", medusaTshirtProduct, adminHeaders)
).data.product
salesChannel = (
await api.post(

View File

@@ -1,6 +1,11 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { PromotionStatus, PromotionType } from "@medusajs/utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
import {
createAdminUser,
generatePublishableKey,
generateStoreHeaders,
} from "../../../../helpers/create-admin-user"
import { medusaTshirtProduct } from "../../../__fixtures__/product"
jest.setTimeout(50000)
@@ -219,6 +224,23 @@ medusaIntegrationTestRunner({
)
})
it("should throw error when an incorrect status is passed", async () => {
const { response } = await api
.post(
`/admin/promotions`,
{ ...standardPromotionPayload, status: "does-not-exist" },
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message:
"Invalid request: Expected: 'draft, active, inactive' for field 'status', but got: 'does-not-exist'",
})
})
it("should create a standard promotion successfully", async () => {
const response = await api.post(
`/admin/promotions`,
@@ -466,20 +488,133 @@ medusaIntegrationTestRunner({
)
})
it("should throw error when an incorrect status is passed", async () => {
const { response } = await api
.post(
describe("with cart", () => {
it("should add promotion to cart only when gte rule matches", async () => {
const publishableKey = await generatePublishableKey(appContainer)
const storeHeaders = generateStoreHeaders({ publishableKey })
const salesChannel = (
await api.post(
"/admin/sales-channels",
{ name: "Webshop", description: "channel" },
adminHeaders
)
).data.sales_channel
const region = (
await api.post(
"/admin/regions",
{ name: "US", currency_code: "usd", countries: ["us"] },
adminHeaders
)
).data.region
const product = (
await api.post(
"/admin/products",
medusaTshirtProduct,
adminHeaders
)
).data.product
const cart = (
await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
promo_codes: [promotion.code],
},
storeHeaders
)
).data.cart
const response = await api.post(
`/admin/promotions`,
{ ...standardPromotionPayload, status: "does-not-exist" },
{
code: "TEST",
type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
is_automatic: true,
application_method: {
target_type: "items",
type: "fixed",
allocation: "each",
currency_code: "USD",
value: 100,
max_quantity: 100,
},
rules: [
{
attribute: "subtotal",
operator: "gte",
values: "2000",
},
],
},
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message:
"Invalid request: Expected: 'draft, active, inactive' for field 'status', but got: 'does-not-exist'",
expect(response.status).toEqual(200)
expect(response.data.promotion).toEqual(
expect.objectContaining({
id: expect.any(String),
code: "TEST",
type: "standard",
is_automatic: true,
application_method: expect.objectContaining({
value: 100,
max_quantity: 100,
type: "fixed",
target_type: "items",
allocation: "each",
target_rules: [],
}),
rules: [
expect.objectContaining({
operator: "gte",
attribute: "subtotal",
values: expect.arrayContaining([
expect.objectContaining({ value: "2000" }),
]),
}),
],
})
)
const cartWithPromotion1 = (
await api.post(
`/store/carts/${cart.id}`,
{ promo_codes: [promotion.code] },
storeHeaders
)
).data.cart
expect(cartWithPromotion1).toEqual(
expect.objectContaining({
promotions: [],
})
)
const cartWithPromotion2 = (
await api.post(
`/store/carts/${cart.id}/line-items`,
{ variant_id: product.variants[0].id, quantity: 40 },
storeHeaders
)
).data.cart
console.log("cartWithPromotion2 -- ", cartWithPromotion2.promotions)
expect(cartWithPromotion2).toEqual(
expect.objectContaining({
promotions: [
expect.objectContaining({
code: response.data.promotion.code,
}),
],
})
)
})
})
})

View File

@@ -0,0 +1,84 @@
import { Modules } from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { evaluateRuleValueCondition } from "../../../../src/utils/validations/promotion-rule"
moduleIntegrationTestRunner({
moduleName: Modules.PROMOTION,
testSuite: () => {
describe("evaluateRuleValueCondition", () => {
const testFunc = evaluateRuleValueCondition
describe("eq", () => {
const operator = "eq"
it("should evaluate conditions accurately", async () => {
expect(testFunc(["2"], operator, [2])).toEqual(true)
expect(testFunc(["2"], operator, ["2"])).toEqual(true)
expect(testFunc(["2"], operator, ["22"])).toEqual(false)
})
})
describe("ne", () => {
const operator = "ne"
it("should evaluate conditions accurately", async () => {
expect(testFunc(["2"], operator, [2])).toEqual(false)
expect(testFunc(["2"], operator, ["2"])).toEqual(false)
expect(testFunc(["2"], operator, ["22"])).toEqual(true)
})
})
describe("gt", () => {
const operator = "gt"
it("should evaluate conditions accurately", async () => {
expect(testFunc(["2"], operator, [1])).toEqual(false)
expect(testFunc(["2"], operator, ["1"])).toEqual(false)
expect(testFunc(["2"], operator, [2])).toEqual(false)
expect(testFunc(["2"], operator, ["2"])).toEqual(false)
expect(testFunc(["2"], operator, ["22"])).toEqual(true)
expect(testFunc(["2"], operator, [22])).toEqual(true)
})
})
describe("gte", () => {
const operator = "gte"
it("should evaluate conditions accurately", async () => {
expect(testFunc(["2"], operator, [1])).toEqual(false)
expect(testFunc(["2"], operator, ["1"])).toEqual(false)
expect(testFunc(["2"], operator, [2])).toEqual(true)
expect(testFunc(["2"], operator, ["2"])).toEqual(true)
expect(testFunc(["2"], operator, ["22"])).toEqual(true)
expect(testFunc(["2"], operator, [22])).toEqual(true)
})
})
describe("lt", () => {
const operator = "lt"
it("should evaluate conditions accurately", async () => {
expect(testFunc([1], operator, ["2"])).toEqual(false)
expect(testFunc(["1"], operator, ["2"])).toEqual(false)
expect(testFunc([2], operator, ["2"])).toEqual(false)
expect(testFunc(["2"], operator, ["2"])).toEqual(false)
expect(testFunc(["22"], operator, ["2"])).toEqual(true)
expect(testFunc([22], operator, ["2"])).toEqual(true)
})
})
describe("lte", () => {
const operator = "lte"
it("should evaluate conditions accurately", async () => {
expect(testFunc([1], operator, ["2"])).toEqual(false)
expect(testFunc(["1"], operator, ["2"])).toEqual(false)
expect(testFunc([2], operator, ["2"])).toEqual(true)
expect(testFunc(["2"], operator, ["2"])).toEqual(true)
expect(testFunc(["22"], operator, ["2"])).toEqual(true)
expect(testFunc([22], operator, ["2"])).toEqual(true)
})
})
})
},
})

View File

@@ -5,6 +5,7 @@ import {
} from "@medusajs/framework/types"
import {
ApplicationMethodTargetType,
MathBN,
MedusaError,
PromotionRuleOperator,
isPresent,
@@ -109,7 +110,7 @@ function fetchRuleAttributeForContext(
export function evaluateRuleValueCondition(
ruleValues: string[],
operator: string,
ruleValuesToCheck: string[] | string
ruleValuesToCheck: (string | number)[] | (string | number)
) {
if (!Array.isArray(ruleValuesToCheck)) {
ruleValuesToCheck = [ruleValuesToCheck]
@@ -119,29 +120,37 @@ export function evaluateRuleValueCondition(
return false
}
return ruleValuesToCheck.every((ruleValueToCheck: string) => {
return ruleValuesToCheck.every((ruleValueToCheck: string | number) => {
if (operator === "in" || operator === "eq") {
return ruleValues.some((ruleValue) => ruleValue === ruleValueToCheck)
return ruleValues.some((ruleValue) => ruleValue === `${ruleValueToCheck}`)
}
if (operator === "ne") {
return ruleValues.some((ruleValue) => ruleValue !== ruleValueToCheck)
return ruleValues.some((ruleValue) => ruleValue !== `${ruleValueToCheck}`)
}
if (operator === "gt") {
return ruleValues.some((ruleValue) => ruleValue > ruleValueToCheck)
return ruleValues.some((ruleValue) =>
MathBN.convert(ruleValueToCheck).gt(MathBN.convert(ruleValue))
)
}
if (operator === "gte") {
return ruleValues.some((ruleValue) => ruleValue >= ruleValueToCheck)
return ruleValues.some((ruleValue) =>
MathBN.convert(ruleValueToCheck).gte(MathBN.convert(ruleValue))
)
}
if (operator === "lt") {
return ruleValues.some((ruleValue) => ruleValue < ruleValueToCheck)
return ruleValues.some((ruleValue) =>
MathBN.convert(ruleValueToCheck).lt(MathBN.convert(ruleValue))
)
}
if (operator === "lte") {
return ruleValues.some((ruleValue) => ruleValue <= ruleValueToCheck)
return ruleValues.some((ruleValue) =>
MathBN.convert(ruleValueToCheck).lte(MathBN.convert(ruleValue))
)
}
return false