Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions src/modules/commerce/commerces/store.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -826,25 +826,52 @@ export const filterStoreProductsService = async (id, filters, pagination) => {
}

const resolvedIsOffer = isOffer ?? is_offer;
let normalizedIsOfferFilter;
if (resolvedIsOffer !== undefined && resolvedIsOffer !== null) {
whereConditions.is_offer =
normalizedIsOfferFilter =
typeof resolvedIsOffer === "boolean"
? resolvedIsOffer
: parseBooleanField(resolvedIsOffer, "isOffer");
whereConditions.is_offer = normalizedIsOfferFilter;
}

const resolvedMinPrice = minPrice ?? price_min;
const resolvedMaxPrice = maxPrice ?? price_max;

const effectivePriceRange = {};
if (resolvedMinPrice !== undefined && resolvedMinPrice !== null) {
whereConditions.price = { gte: Number(resolvedMinPrice) };
effectivePriceRange.gte = Number(resolvedMinPrice);
}

if (resolvedMaxPrice !== undefined && resolvedMaxPrice !== null) {
whereConditions.price = {
...whereConditions.price,
lte: Number(resolvedMaxPrice)
};
effectivePriceRange.lte = Number(resolvedMaxPrice);
}

if (Object.keys(effectivePriceRange).length > 0) {
const effectivePriceBranches = [];

if (normalizedIsOfferFilter !== true) {
effectivePriceBranches.push({
AND: [
{ is_offer: false },
{ price: effectivePriceRange }
]
});
}

if (normalizedIsOfferFilter !== false) {
effectivePriceBranches.push({
AND: [
{ is_offer: true },
{ offer_price: effectivePriceRange }
]
});
}

whereConditions.OR = [
...(Array.isArray(whereConditions.OR) ? whereConditions.OR : []),
...effectivePriceBranches
];
}
Comment on lines +841 to 875

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validá price_min y price_max antes de armar el rango.

Acá Number("") termina en 0 y Number("abc") en NaN. Con el fallback a req.query, eso puede abrir el filtro de más o mandar un rango inválido a Prisma en vez de rechazar la request con 400. También conviene rechazar price_min > price_max antes de construir el OR.

🛠️ Propuesta de ajuste
     const resolvedMinPrice = minPrice ?? price_min;
     const resolvedMaxPrice = maxPrice ?? price_max;
 
+    const parseOptionalPrice = (value, fieldName) => {
+      if (value === undefined || value === null) {
+        return undefined;
+      }
+
+      if (typeof value === "string" && value.trim() === "") {
+        return undefined;
+      }
+
+      const parsedValue = Number(value);
+      if (!Number.isFinite(parsedValue)) {
+        throw { status: 400, message: `${fieldName} invalido` };
+      }
+
+      return parsedValue;
+    };
+
-    const effectivePriceRange = {};
-    if (resolvedMinPrice !== undefined && resolvedMinPrice !== null) {
-      effectivePriceRange.gte = Number(resolvedMinPrice);
-    }
+    const parsedMinPrice = parseOptionalPrice(resolvedMinPrice, "price_min");
+    const parsedMaxPrice = parseOptionalPrice(resolvedMaxPrice, "price_max");
+
+    if (
+      parsedMinPrice !== undefined &&
+      parsedMaxPrice !== undefined &&
+      parsedMinPrice > parsedMaxPrice
+    ) {
+      throw { status: 400, message: "price_min no puede ser mayor que price_max" };
+    }
+
+    const effectivePriceRange = {};
+    if (parsedMinPrice !== undefined) {
+      effectivePriceRange.gte = parsedMinPrice;
+    }
 
-    if (resolvedMaxPrice !== undefined && resolvedMaxPrice !== null) {
-      effectivePriceRange.lte = Number(resolvedMaxPrice);
+    if (parsedMaxPrice !== undefined) {
+      effectivePriceRange.lte = parsedMaxPrice;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/commerce/commerces/store.service.js` around lines 841 - 875,
Valida resolvedMinPrice y resolvedMaxPrice antes de construir
effectivePriceRange: rechaza valores vacíos o no numéricos (evitar que
Number("") => 0 o Number("abc") => NaN) y responde con 400; convierte sólo
cuando Number.isFinite(Number(value)) es true; además, si ambos existen valida
que Number(resolvedMinPrice) <= Number(resolvedMaxPrice) y rechaza con 400 si
no; después de estas comprobaciones procede a poblar effectivePriceRange y
construir las ramas que usan normalizedIsOfferFilter y whereConditions.OR como
ahora.


const [totalProducts, products] = await Promise.all([
Expand Down
39 changes: 39 additions & 0 deletions tests/e2e/stores.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,45 @@ describe("GET /api/commerces/products/filter/:id", () => {
expect(Array.isArray(res.body.content)).toBe(true);
});

it("aplica el rango sobre price u offer_price segun el precio efectivo", async () => {
prisma.stores.findUnique.mockResolvedValue({ id_store: 1 });
prisma.products.count.mockResolvedValue(1);
prisma.products.findMany.mockResolvedValue([
{
...mockProducts[0],
price: 20,
offer_price: 12,
is_offer: true
}
]);

const res = await request(app).get(
"/api/commerces/products/filter/1?price_min=10&price_max=15"
);

expect(res.status).toBe(200);
expect(prisma.products.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
OR: [
{
AND: [
{ is_offer: false },
{ price: { gte: 10, lte: 15 } }
]
},
{
AND: [
{ is_offer: true },
{ offer_price: { gte: 10, lte: 15 } }
]
}
]
})
})
);
});

it("devuelve 404 cuando no hay productos con los filtros aplicados", async () => {
prisma.stores.findUnique.mockResolvedValue({ id_store: 1 });
prisma.products.count.mockResolvedValue(0);
Expand Down
Loading