fix(admin-products): add class-validator ProductDto to allow product creation and updates through ValidationPipe
All checks were successful
Deploy Canina / deploy (push) Successful in 32s

This commit is contained in:
parsa aghaei 2026-08-18 10:34:52 +03:30
parent 4cbde63b89
commit d5c05b7577
3 changed files with 176 additions and 33 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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;
}