From 3497a743b3f2afaf441717945beec348ca7f3078 Mon Sep 17 00:00:00 2001
From: parsa aghaei
Date: Wed, 5 Aug 2026 14:39:58 +0330
Subject: [PATCH] feat(products): fix search query filter overwrite and add
minPrice maxPrice sorting filters [TASK-26.10] [TASK-26.11]
---
backend/src/products/dto/get-products.dto.ts | 20 ++++++
backend/src/products/products.service.ts | 72 ++++++++++----------
docs/backlog.md | 4 +-
3 files changed, 59 insertions(+), 37 deletions(-)
diff --git a/backend/src/products/dto/get-products.dto.ts b/backend/src/products/dto/get-products.dto.ts
index f268e30..9c9f77d 100644
--- a/backend/src/products/dto/get-products.dto.ts
+++ b/backend/src/products/dto/get-products.dto.ts
@@ -25,4 +25,24 @@ export class GetProductsDto extends PaginationDto {
@IsOptional()
@IsString()
requiresRx?: string;
+
+ @ApiPropertyOptional({ description: 'حداقل قیمت (تومان)' })
+ @IsOptional()
+ @IsString()
+ minPrice?: string;
+
+ @ApiPropertyOptional({ description: 'حداکثر قیمت (تومان)' })
+ @IsOptional()
+ @IsString()
+ maxPrice?: string;
+
+ @ApiPropertyOptional({ description: 'فیلد مرتبسازی', enum: ['priceValue', 'createdAt', 'nameFa'] })
+ @IsOptional()
+ @IsString()
+ sortBy?: string;
+
+ @ApiPropertyOptional({ description: 'جهت مرتبسازی', enum: ['asc', 'desc'] })
+ @IsOptional()
+ @IsEnum(['asc', 'desc'])
+ sortOrder?: 'asc' | 'desc';
}
diff --git a/backend/src/products/products.service.ts b/backend/src/products/products.service.ts
index d7c051d..be8bba7 100644
--- a/backend/src/products/products.service.ts
+++ b/backend/src/products/products.service.ts
@@ -13,66 +13,68 @@ export class ProductsService {
search,
symptom,
requiresRx,
+ minPrice,
+ maxPrice,
page = 1,
limit = 10,
sortBy = 'createdAt',
sortOrder = 'desc',
} = filters;
- const whereClause: any = {};
+ const andConditions: any[] = [];
if (requiresRx !== undefined && requiresRx !== null && requiresRx !== '') {
- whereClause.requiresRx = requiresRx === 'true' || requiresRx === '1';
+ andConditions.push({ requiresRx: requiresRx === 'true' || requiresRx === '1' });
}
if (category) {
- whereClause.categorySlug = category;
+ andConditions.push({ categorySlug: category });
}
if (petType && petType !== 'all') {
- whereClause.suitableFor = { in: [petType, 'هر دو'] };
+ andConditions.push({ suitableFor: { in: [petType, 'هر دو'] } });
}
- // Filter by a specific symptom (from URL param ?symptom=...)
if (symptom) {
- whereClause.symptoms = {
- some: {
- symptom: { contains: symptom, mode: 'insensitive' },
+ andConditions.push({
+ symptoms: {
+ some: {
+ symptom: { contains: symptom, mode: 'insensitive' },
+ },
},
- };
+ });
+ }
+
+ if (minPrice) {
+ andConditions.push({ priceValue: { gte: Number(minPrice) } });
+ }
+
+ if (maxPrice) {
+ andConditions.push({ priceValue: { lte: Number(maxPrice) } });
}
if (search) {
- // If symptom filter is already applied, extend via AND to also search names/desc
- // If not, use OR across names, description AND symptoms
- const searchConditions = [
- { artNo: { contains: search, mode: 'insensitive' } },
- { barcode: { contains: search, mode: 'insensitive' } },
- { nameFa: { contains: search, mode: 'insensitive' } },
- { nameEn: { contains: search, mode: 'insensitive' } },
- { description: { contains: search, mode: 'insensitive' } },
- { shortDescription: { contains: search, mode: 'insensitive' } },
- {
- symptoms: {
- some: {
- symptom: { contains: search, mode: 'insensitive' },
+ andConditions.push({
+ OR: [
+ { artNo: { contains: search, mode: 'insensitive' } },
+ { barcode: { contains: search, mode: 'insensitive' } },
+ { nameFa: { contains: search, mode: 'insensitive' } },
+ { nameEn: { contains: search, mode: 'insensitive' } },
+ { description: { contains: search, mode: 'insensitive' } },
+ { shortDescription: { contains: search, mode: 'insensitive' } },
+ {
+ symptoms: {
+ some: {
+ symptom: { contains: search, mode: 'insensitive' },
+ },
},
},
- },
- ];
-
- if (symptom) {
- // Already have a symptom filter; combine with AND
- whereClause.AND = [
- { symptoms: whereClause.symptoms },
- { OR: searchConditions.filter((c) => !('symptoms' in c)) },
- ];
- delete whereClause.symptoms;
- } else {
- whereClause.OR = searchConditions;
- }
+ ],
+ });
}
+ const whereClause: any = andConditions.length > 0 ? { AND: andConditions } : {};
+
const skip = (page - 1) * limit;
const [rawProducts, total] = await Promise.all([
diff --git a/docs/backlog.md b/docs/backlog.md
index 00ca1e6..7be458b 100644
--- a/docs/backlog.md
+++ b/docs/backlog.md
@@ -207,8 +207,8 @@
- [ ] **TASK-26.7 [SSR & HttpOnly Cookie Auth]:** ذخیره توکنها درون HttpOnly Cookie جهت همگامسازی کامل Server & Client Components در Next.js.
- [x] **TASK-26.8 [Logout State Cleanup]:** پاکسازی کامل کاتالوگ، سبد خرید و تمام کشهای کاربر هنگام خروج (Logout Cleanup).
- [x] **TASK-26.9 [Admin Token Validation Guard]:** اعتبارسنجی واقعی توکن ادمین در `ProtectedRoute.tsx` از طریق فراخوانی API پروفایل در هنگام لود پنل.
-- [ ] **TASK-26.10 [Fix Pagination & Search Query Overwrite]:** اصلاح منطق `whereClause` در `products.service.ts` جهت جلوگیری از اوررایت شدن فیلترها.
-- [ ] **TASK-26.11 [Price Range & Sorting Filters]:** افزودن فیلتر محدوده قیمت (`minPrice`, `maxPrice`) و مرتبسازی (`sort`) به DTO و اندپویینتهای محصولات.
+- [x] **TASK-26.10 [Fix Pagination & Search Query Overwrite]:** اصلاح منطق `whereClause` در `products.service.ts` جهت جلوگیری از اوررایت شدن فیلترها.
+- [x] **TASK-26.11 [Price Range & Sorting Filters]:** افزودن فیلتر محدوده قیمت (`minPrice`, `maxPrice`) و مرتبسازی (`sort`) به DTO و اندپویینتهای محصولات.
- [ ] **TASK-26.12 [Sync Fallback IDs with Database]:** همگامسازی شناسه محصولات استاتیک فرانتاند با دیتابیس واقعی جهت جلوگیری از 404 در سفارش.
- [ ] **TASK-26.13 [Admin Products Table Pagination]:** پیادهسازی پجینیشن سرورساید کامل در جدول مدیریت محصولات پنل ادمین.
- [ ] **TASK-26.14 [Admin Wallet Adjust Endpoint]:** ایجاد اندپویینت `POST /api/users/:id/wallet-adjust` و فرم شارژ/کسر مستقیم کیفپول در پنل ادمین.