From d5c05b757764dcd843e2b9c6443c4e1595b22275 Mon Sep 17 00:00:00 2001
From: parsa aghaei
Date: Tue, 18 Aug 2026 10:34:52 +0330
Subject: [PATCH] fix(admin-products): add class-validator ProductDto to allow
product creation and updates through ValidationPipe
---
backend/src/admin/admin.controller.ts | 7 +-
backend/src/admin/admin.service.ts | 37 ++----
backend/src/admin/dto/product.dto.ts | 165 ++++++++++++++++++++++++++
3 files changed, 176 insertions(+), 33 deletions(-)
create mode 100644 backend/src/admin/dto/product.dto.ts
diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts
index 2de1352..994b041 100644
--- a/backend/src/admin/admin.controller.ts
+++ b/backend/src/admin/admin.controller.ts
@@ -9,7 +9,8 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
-import { AdminService, ProductInput, CouponInput } from './admin.service';
+import { AdminService, CouponInput } from './admin.service';
+import { ProductDto } from './dto/product.dto';
import { AdminQueryDto } from './dto/admin-query.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import {
@@ -127,14 +128,14 @@ export class AdminController {
@Post('products')
@ApiOperation({ summary: 'ایجاد محصول جدید' })
- async createProduct(@Body() data: ProductInput) {
+ async createProduct(@Body() data: ProductDto) {
const product = await this.adminService.createProduct(data);
return { success: true, data: product };
}
@Put('products/:id')
@ApiOperation({ summary: 'ویرایش محصول' })
- async updateProduct(@Param('id') id: string, @Body() data: ProductInput) {
+ async updateProduct(@Param('id') id: string, @Body() data: ProductDto) {
const product = await this.adminService.updateProduct(id, data);
return {
success: true,
diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts
index d0bdbbb..6d2793f 100644
--- a/backend/src/admin/admin.service.ts
+++ b/backend/src/admin/admin.service.ts
@@ -9,6 +9,7 @@ import { PrismaService } from '../prisma/prisma.service';
import { RedisService } from '../redis/redis.service';
import * as bcrypt from 'bcryptjs';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
+import { ProductDto } from './dto/product.dto';
import { normalizeMobile } from '../common/utils/phone.utils';
import { CANONICAL_ALIASES } from '../settings/settings.service';
@@ -21,34 +22,6 @@ export class PaginationQuery {
status?: string;
}
-export class ProductInput {
- artNo?: string;
- nameFa?: string;
- nameEn?: string;
- scientificTagline?: string;
- description?: string;
- shortDescription?: string;
- categoryId?: string;
- categorySlug?: string;
- priceValue?: number;
- priceDisplay?: string;
- unit?: string;
- packageSize?: number;
- dosageLogic?: string;
- suitableFor?: string;
- imageUrl?: string;
- images?: string | string[];
- podcastUrl?: string | null;
- videoUrl?: string | null;
- pdfUrl?: string | null;
- metaTitle?: string;
- metaDescription?: string;
- keywords?: string;
- canonicalUrl?: string;
- slug?: string;
- symptoms?: string[];
-}
-
export class CouponTargetInput {
targetType!: string;
targetId!: string;
@@ -333,7 +306,7 @@ export class AdminService {
}
}
- async createProduct(data: ProductInput) {
+ async createProduct(data: ProductDto) {
const product = await this.prisma.product.create({
data: {
artNo: data.artNo || `ART-${Date.now()}`,
@@ -344,7 +317,9 @@ export class AdminService {
shortDescription: data.shortDescription || '',
categoryId: data.categoryId || '',
categorySlug: data.categorySlug || 'general',
+ buyPrice: data.buyPrice !== undefined ? data.buyPrice : 0,
priceValue: data.priceValue || 0,
+ wholesalePrice: data.wholesalePrice !== undefined ? data.wholesalePrice : null,
priceDisplay: data.priceDisplay || '',
unit: data.unit || 'عدد',
packageSize: data.packageSize || 100,
@@ -381,7 +356,7 @@ export class AdminService {
return product;
}
- async updateProduct(id: string, data: ProductInput) {
+ async updateProduct(id: string, data: ProductDto) {
const existing = await this.prisma.product.findUnique({ where: { id } });
if (!existing) {
throw new NotFoundException(`محصولی با شناسه ${id} یافت نشد.`);
@@ -398,7 +373,9 @@ export class AdminService {
shortDescription: data.shortDescription,
categoryId: data.categoryId,
categorySlug: data.categorySlug,
+ buyPrice: data.buyPrice !== undefined ? data.buyPrice : undefined,
priceValue: data.priceValue,
+ wholesalePrice: data.wholesalePrice !== undefined ? data.wholesalePrice : undefined,
priceDisplay: data.priceDisplay,
unit: data.unit,
packageSize: data.packageSize,
diff --git a/backend/src/admin/dto/product.dto.ts b/backend/src/admin/dto/product.dto.ts
new file mode 100644
index 0000000..03799b6
--- /dev/null
+++ b/backend/src/admin/dto/product.dto.ts
@@ -0,0 +1,165 @@
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
+import {
+ IsString,
+ IsOptional,
+ IsNumber,
+ IsArray,
+ IsBoolean,
+} from 'class-validator';
+
+export class ProductDto {
+ @ApiPropertyOptional({ description: 'شماره آرتیکل محصول' })
+ @IsOptional()
+ @IsString()
+ artNo?: string;
+
+ @ApiPropertyOptional({ description: 'نام فارسی محصول' })
+ @IsOptional()
+ @IsString()
+ nameFa?: string;
+
+ @ApiPropertyOptional({ description: 'نام انگلیسی محصول' })
+ @IsOptional()
+ @IsString()
+ nameEn?: string;
+
+ @ApiPropertyOptional({ description: 'شعار علمی محصول' })
+ @IsOptional()
+ @IsString()
+ scientificTagline?: string;
+
+ @ApiPropertyOptional({ description: 'توضیحات کامل محصول' })
+ @IsOptional()
+ @IsString()
+ description?: string;
+
+ @ApiPropertyOptional({ description: 'توضیحات کوتاه محصول' })
+ @IsOptional()
+ @IsString()
+ shortDescription?: string;
+
+ @ApiPropertyOptional({ description: 'شناسه دستهبندی' })
+ @IsOptional()
+ @IsString()
+ categoryId?: string;
+
+ @ApiPropertyOptional({ description: 'اسلاگ دستهبندی' })
+ @IsOptional()
+ @IsString()
+ categorySlug?: string;
+
+ @ApiPropertyOptional({ description: 'قیمت خرید محصول' })
+ @IsOptional()
+ @IsNumber()
+ buyPrice?: number;
+
+ @ApiPropertyOptional({ description: 'قیمت فروش محصول' })
+ @IsOptional()
+ @IsNumber()
+ priceValue?: number;
+
+ @ApiPropertyOptional({ description: 'قیمت عمدهفروشی محصول' })
+ @IsOptional()
+ @IsNumber()
+ wholesalePrice?: number;
+
+ @ApiPropertyOptional({ description: 'درصد سود قیمت مصرفکننده' })
+ @IsOptional()
+ @IsNumber()
+ priceValueMarginPercent?: number;
+
+ @ApiPropertyOptional({ description: 'درصد سود قیمت عمدهفروشی' })
+ @IsOptional()
+ @IsNumber()
+ wholesaleMarginPercent?: number;
+
+ @ApiPropertyOptional({ description: 'نمایش متنی قیمت' })
+ @IsOptional()
+ @IsString()
+ priceDisplay?: string;
+
+ @ApiPropertyOptional({ description: 'واحد شمارش محصول' })
+ @IsOptional()
+ @IsString()
+ unit?: string;
+
+ @ApiPropertyOptional({ description: 'اندازه و وزن بسته' })
+ @IsOptional()
+ @IsNumber()
+ packageSize?: number;
+
+ @ApiPropertyOptional({ description: 'منطق دوزبندی و نحوه مصرف' })
+ @IsOptional()
+ @IsString()
+ dosageLogic?: string;
+
+ @ApiPropertyOptional({ description: 'مناسب برای گونه حیوان' })
+ @IsOptional()
+ @IsString()
+ suitableFor?: string;
+
+ @ApiPropertyOptional({ description: 'لینک تصویر اصلی محصول' })
+ @IsOptional()
+ @IsString()
+ imageUrl?: string;
+
+ @ApiPropertyOptional({ description: 'گالری تصاویر محصول' })
+ @IsOptional()
+ @IsArray()
+ images?: string[];
+
+ @ApiPropertyOptional({ description: 'لینک پادکست' })
+ @IsOptional()
+ @IsString()
+ podcastUrl?: string | null;
+
+ @ApiPropertyOptional({ description: 'لینک ویدیو' })
+ @IsOptional()
+ @IsString()
+ videoUrl?: string | null;
+
+ @ApiPropertyOptional({ description: 'لینک فایل PDF' })
+ @IsOptional()
+ @IsString()
+ pdfUrl?: string | null;
+
+ @ApiPropertyOptional({ description: 'عنوان سئو' })
+ @IsOptional()
+ @IsString()
+ metaTitle?: string;
+
+ @ApiPropertyOptional({ description: 'توضیحات متای سئو' })
+ @IsOptional()
+ @IsString()
+ metaDescription?: string;
+
+ @ApiPropertyOptional({ description: 'کلمات کلیدی سئو' })
+ @IsOptional()
+ @IsString()
+ keywords?: string;
+
+ @ApiPropertyOptional({ description: 'آدرس کانونیکال سئو' })
+ @IsOptional()
+ @IsString()
+ canonicalUrl?: string;
+
+ @ApiPropertyOptional({ description: 'اسلاگ محصول' })
+ @IsOptional()
+ @IsString()
+ slug?: string;
+
+ @ApiPropertyOptional({ description: 'علائم درمانی مرتبط' })
+ @IsOptional()
+ @IsArray()
+ symptoms?: string[];
+
+ @ApiPropertyOptional({ description: 'آیا پیشخرید فعال است؟' })
+ @IsOptional()
+ @IsBoolean()
+ isPreorder?: boolean;
+
+ @ApiPropertyOptional({ description: 'مبلغ بیعانه پیشخرید' })
+ @IsOptional()
+ @IsString()
+ preorderDeposit?: string;
+}