From 1e6787abdd638e3b5c3fa7b462bc737bf7b7f5e4 Mon Sep 17 00:00:00 2001 From: safavimohadeseh78 Date: Wed, 30 Sep 2026 22:12:35 +0330 Subject: [PATCH] change nademitex2 --- .../migration.sql | 1 + prisma/schema.prisma | 17 -- src/app.module.ts | 2 - ...preserve-product-colors-json.middleware.ts | 23 +++ src/main.ts | 2 + src/products/color-entries.spec.ts | 14 ++ src/products/dto/product-color-json.dto.ts | 20 +++ src/products/dto/product.dto.ts | 128 +-------------- src/products/products.controller.ts | 17 +- src/products/products.service.ts | 31 +++- src/stock-notify/stock-notify.controller.ts | 50 ------ src/stock-notify/stock-notify.module.ts | 10 -- src/stock-notify/stock-notify.service.ts | 154 ------------------ 13 files changed, 102 insertions(+), 367 deletions(-) create mode 100644 prisma/migrations/20260930230000_drop_stock_notify_request/migration.sql create mode 100644 src/common/middleware/preserve-product-colors-json.middleware.ts delete mode 100644 src/stock-notify/stock-notify.controller.ts delete mode 100644 src/stock-notify/stock-notify.module.ts delete mode 100644 src/stock-notify/stock-notify.service.ts diff --git a/prisma/migrations/20260930230000_drop_stock_notify_request/migration.sql b/prisma/migrations/20260930230000_drop_stock_notify_request/migration.sql new file mode 100644 index 0000000..a3c8aea --- /dev/null +++ b/prisma/migrations/20260930230000_drop_stock_notify_request/migration.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "StockNotifyRequest"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9f6610e..c2f43d3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -204,7 +204,6 @@ model Product { orderItems OrderItem[] favorites ProductFavorite[] catalogRequestItems CatalogRequestItem[] - stockNotifyRequests StockNotifyRequest[] productSuggestions ProductSuggestion[] @@unique([companyId, code]) @@ -254,7 +253,6 @@ model Customer { remindersAbout Reminder[] @relation("ReminderCustomer") favorites ProductFavorite[] catalogRequests CatalogRequest[] - stockNotifyRequests StockNotifyRequest[] productSuggestions ProductSuggestion[] customerBrands CustomerBrand[] addresses CustomerAddress[] @@ -724,21 +722,6 @@ model CatalogItemMovement { @@index([itemId]) } -model StockNotifyRequest { - id String @id @default(cuid()) - customerId String - productId String - active Boolean @default(true) - createdAt DateTime @default(now()) - notifiedAt DateTime? - - customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) - product Product @relation(fields: [productId], references: [id], onDelete: Cascade) - - @@unique([customerId, productId]) - @@index([productId, active]) -} - model ProductSuggestion { id String @id @default(cuid()) customerId String diff --git a/src/app.module.ts b/src/app.module.ts index 7ce2aa6..edf6bd0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,7 +22,6 @@ import { CatalogRequestsModule } from './catalog-requests/catalog-requests.modul import { BrandsModule } from './brands/brands.module'; import { LevelsModule } from './levels/levels.module'; import { SuggestionsModule } from './suggestions/suggestions.module'; -import { StockNotifyModule } from './stock-notify/stock-notify.module'; import { BannersModule } from './banners/banners.module'; import { AddressesModule } from './addresses/addresses.module'; import { InternalMessagesModule } from './internal-messages/internal-messages.module'; @@ -50,7 +49,6 @@ import { InternalMessagesModule } from './internal-messages/internal-messages.mo BrandsModule, LevelsModule, SuggestionsModule, - StockNotifyModule, BannersModule, AddressesModule, InternalMessagesModule, diff --git a/src/common/middleware/preserve-product-colors-json.middleware.ts b/src/common/middleware/preserve-product-colors-json.middleware.ts new file mode 100644 index 0000000..0472d50 --- /dev/null +++ b/src/common/middleware/preserve-product-colors-json.middleware.ts @@ -0,0 +1,23 @@ +import type { NextFunction, Request, Response } from 'express'; + +/** قبل از ValidationPipe — نگه‌داشتن colorsJson کامل (شامل imageUrl/base64) */ +export function preserveProductColorsJsonMiddleware( + req: Request, + _res: Response, + next: NextFunction, +) { + const body = req.body; + if (body && typeof body === 'object' && 'colorsJson' in body) { + const raw = (body as { colorsJson?: unknown }).colorsJson; + if (raw !== undefined) { + try { + (req as Request & { preservedColorsJson?: unknown }).preservedColorsJson = + JSON.parse(JSON.stringify(raw)); + } catch { + (req as Request & { preservedColorsJson?: unknown }).preservedColorsJson = + raw; + } + } + } + next(); +} diff --git a/src/main.ts b/src/main.ts index 504774d..5d5ec1f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { json, urlencoded } from 'express'; import { AppModule } from './app.module'; import { applyPendingMigrations } from './apply-migrations'; import { runBundledSeed } from './run-seed'; +import { preserveProductColorsJsonMiddleware } from './common/middleware/preserve-product-colors-json.middleware'; function swaggerUiAssetsPath() { const bundled = path.join(process.cwd(), 'runtime', 'swagger-ui'); @@ -30,6 +31,7 @@ async function bootstrap() { const bodyLimit = process.env.BODY_LIMIT || '25mb'; app.use(json({ limit: bodyLimit })); app.use(urlencoded({ extended: true, limit: bodyLimit })); + app.use(preserveProductColorsJsonMiddleware); // پشت LiteSpeed روی /server → GLOBAL_PREFIX=server/api const globalPrefix = process.env.GLOBAL_PREFIX || 'server/api'; diff --git a/src/products/color-entries.spec.ts b/src/products/color-entries.spec.ts index 1bd6c6f..8022574 100644 --- a/src/products/color-entries.spec.ts +++ b/src/products/color-entries.spec.ts @@ -3,6 +3,7 @@ import { parseColorEntries, parseGeneralImageUrls, } from './color-entries'; +import { serializeColorsJsonForDb } from './dto/product-color-json.dto'; describe('product image separation', () => { it('keeps general images separate from color variant urls', () => { @@ -24,6 +25,19 @@ describe('product image separation', () => { ]); }); + it('serializes color imageUrl for database', () => { + const db = serializeColorsJsonForDb([ + { name: 'قرمز', imageUrl: 'data:image/jpeg;base64,abc' }, + ]); + expect(db).toEqual([ + { + name: 'قرمز', + imageUrl: 'data:image/jpeg;base64,abc', + imageUrls: ['data:image/jpeg;base64,abc'], + }, + ]); + }); + it('does not merge color images into general list when only imageUrlsJson is set', () => { const entries = parseColorEntries([{ name: 'Red', imageUrl: 'https://cdn/r.jpg' }]); expect(parseGeneralImageUrls(['https://cdn/g.jpg'], entries)).toEqual([ diff --git a/src/products/dto/product-color-json.dto.ts b/src/products/dto/product-color-json.dto.ts index 7de5e87..1961161 100644 --- a/src/products/dto/product-color-json.dto.ts +++ b/src/products/dto/product-color-json.dto.ts @@ -49,3 +49,23 @@ export function transformColorsJson(value: unknown): ProductColorJsonItemDto[] | } return out; } + +/** ذخیره در Prisma — imageUrl + imageUrls برای هر رنگ */ +export function serializeColorsJsonForDb(raw: unknown): unknown[] | undefined { + const entries = transformColorsJson(raw); + if (entries === undefined) return undefined; + return entries.map((entry) => { + const merged: string[] = []; + const push = (u?: string) => { + const t = u?.trim(); + if (t && !merged.includes(t)) merged.push(t); + }; + for (const u of entry.imageUrls ?? []) push(u); + push(entry.imageUrl); + const first = merged[0]; + return { + name: entry.name, + ...(first ? { imageUrl: first, imageUrls: merged } : {}), + }; + }); +} diff --git a/src/products/dto/product.dto.ts b/src/products/dto/product.dto.ts index 82748ce..93a6760 100644 --- a/src/products/dto/product.dto.ts +++ b/src/products/dto/product.dto.ts @@ -1,3 +1,4 @@ +import { OmitType, PartialType } from '@nestjs/mapped-types'; import { IsArray, IsBoolean, @@ -151,135 +152,16 @@ export class CreateProductDto { tagsJson?: string[]; } -export class UpdateProductDto { - @IsOptional() - @IsString() - brandId?: string | null; - - @IsOptional() - @IsString() - @MinLength(1) - name?: string; - - @IsOptional() - @IsString() - @MinLength(1) - code?: string; - - @IsOptional() - @IsString() - productCode?: string; - - @IsOptional() - @IsString() - color?: string; - - @IsOptional() - @IsString() - description?: string | null; - - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(0) - minMeterage?: number; - - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(0.001) - orderMultiplier?: number; - - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(0.001) - orderStep?: number; - - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(0) - basePrice?: number | null; - - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(0) - price?: number | null; - - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(0) - stock?: number | null; - - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(0) - internalStock?: number | null; - - @IsOptional() - @IsBoolean() - isActive?: boolean; - - @IsOptional() - @IsString() - usageCategory?: string; - - @IsOptional() - @IsString() - season?: string; - - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(0) - widthCm?: number | null; - - @IsOptional() - @IsString() - composition?: string | null; - - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(0) - weightGrams?: number | null; - - @IsOptional() - @IsString() - weaveType?: string | null; - - @IsOptional() - @IsString() - subtitle?: string | null; - - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(0) - rollMeters?: number; - - @IsOptional() - @IsBoolean() - isNew?: boolean; - +/** همهٔ فیلدهای Create به‌جز companyId — شامل brandId برای PATCH ادمین */ +export class UpdateProductDto extends PartialType( + OmitType(CreateProductDto, ['companyId'] as const), +) { @IsOptional() @Transform(({ value }) => transformColorsJson(value)) @IsArray() @ValidateNested({ each: true }) @Type(() => ProductColorJsonItemDto) colorsJson?: ProductColorJsonItemDto[]; - - @IsOptional() - @IsArray() - @IsString({ each: true }) - imageUrlsJson?: string[]; - - @IsOptional() - tagsJson?: string[]; } export class SetProductActiveDto { diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 60177a8..a89725d 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -7,7 +7,9 @@ import { Patch, Post, Query, + Req, } from '@nestjs/common'; +import type { Request } from 'express'; import { ProductsService } from './products.service'; import { AuthType } from '../common/decorators/auth.decorators'; import { CurrentUser } from '../common/decorators/current-user.decorator'; @@ -85,14 +87,21 @@ export class ProductsController { @AuthType('admin') @Post() - create(@Body() dto: CreateProductDto) { - return this.productsService.create(dto); + create( + @Body() dto: CreateProductDto, + @Req() req: Request & { preservedColorsJson?: unknown }, + ) { + return this.productsService.create(dto, req.preservedColorsJson); } @AuthType('admin') @Patch(':id') - update(@Param('id') id: string, @Body() dto: UpdateProductDto) { - return this.productsService.update(id, dto); + update( + @Param('id') id: string, + @Body() dto: UpdateProductDto, + @Req() req: Request & { preservedColorsJson?: unknown }, + ) { + return this.productsService.update(id, dto, req.preservedColorsJson); } @AuthType('admin') diff --git a/src/products/products.service.ts b/src/products/products.service.ts index 72372eb..893f069 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -7,6 +7,7 @@ import { import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { CreateProductDto, UpdateProductDto } from './dto/product.dto'; +import { serializeColorsJsonForDb } from './dto/product-color-json.dto'; import { loadCatalogCategories } from '../catalog/category-defs'; import { colorVariantImageLists, @@ -75,7 +76,18 @@ export class ProductsService { return false; } - async create(dto: CreateProductDto) { + private colorsJsonForWrite( + preserved: unknown | undefined, + fromDto: unknown | undefined, + ): Prisma.InputJsonValue | undefined { + const source = preserved !== undefined ? preserved : fromDto; + if (source === undefined) return undefined; + const serialized = serializeColorsJsonForDb(source); + if (serialized === undefined) return undefined; + return serialized as Prisma.InputJsonValue; + } + + async create(dto: CreateProductDto, preservedColorsJson?: unknown) { const code = (dto.code || dto.productCode || '').trim(); if (!code) throw new ConflictException('کد محصول الزامی است'); const brandId = await this.resolveBrandId(dto.brandId ?? null); @@ -103,9 +115,10 @@ export class ProductsService { subtitle: dto.subtitle?.trim() || null, rollMeters: dto.rollMeters, isNew: dto.isNew, - colorsJson: (dto.colorsJson ?? undefined) as unknown as - | Prisma.InputJsonValue - | undefined, + colorsJson: this.colorsJsonForWrite( + preservedColorsJson, + dto.colorsJson, + ), imageUrlsJson: (dto.imageUrlsJson ?? undefined) as Prisma.InputJsonValue | undefined, tagsJson: dto.tagsJson ?? undefined, }, @@ -123,7 +136,11 @@ export class ProductsService { } } - async update(id: string, dto: UpdateProductDto) { + async update( + id: string, + dto: UpdateProductDto, + preservedColorsJson?: unknown, + ) { await this.ensure(id); const code = dto.code !== undefined @@ -185,9 +202,9 @@ export class ProductsService { rollMeters: dto.rollMeters, isNew: dto.isNew, colorsJson: - dto.colorsJson === undefined + dto.colorsJson === undefined && preservedColorsJson === undefined ? undefined - : (dto.colorsJson as unknown as Prisma.InputJsonValue), + : this.colorsJsonForWrite(preservedColorsJson, dto.colorsJson), imageUrlsJson: dto.imageUrlsJson === undefined ? undefined diff --git a/src/stock-notify/stock-notify.controller.ts b/src/stock-notify/stock-notify.controller.ts deleted file mode 100644 index fbc0824..0000000 --- a/src/stock-notify/stock-notify.controller.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common'; -import { IsString, MinLength } from 'class-validator'; -import { StockNotifyService } from './stock-notify.service'; -import { AuthType } from '../common/decorators/auth.decorators'; -import { CurrentUser } from '../common/decorators/current-user.decorator'; -import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; - -class SubscribeDto { - @IsString() - @MinLength(1) - productId!: string; -} - -@Controller() -export class StockNotifyController { - constructor(private readonly stockNotifyService: StockNotifyService) {} - - @AuthType('admin') - @Get('admin/stock-alerts') - list(@Query('activeOnly') activeOnly?: string) { - return this.stockNotifyService.listAdmin(activeOnly !== 'false'); - } - - @AuthType('admin') - @Patch('admin/stock-alerts/:id/notify') - notify(@Param('id') id: string) { - return this.stockNotifyService.markNotified(id); - } - - @AuthType('customer') - @Get('stock-alerts') - listMine(@CurrentUser() user: AuthPrincipal) { - return this.stockNotifyService.listForCustomer(user.id); - } - - @AuthType('customer') - @Post('stock-alerts') - subscribe(@CurrentUser() user: AuthPrincipal, @Body() dto: SubscribeDto) { - return this.stockNotifyService.subscribe(user.id, dto.productId); - } - - @AuthType('customer') - @Post('stock-alerts/:productId/cancel') - unsubscribe( - @CurrentUser() user: AuthPrincipal, - @Param('productId') productId: string, - ) { - return this.stockNotifyService.unsubscribe(user.id, productId); - } -} diff --git a/src/stock-notify/stock-notify.module.ts b/src/stock-notify/stock-notify.module.ts deleted file mode 100644 index 38a0d1a..0000000 --- a/src/stock-notify/stock-notify.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { StockNotifyController } from './stock-notify.controller'; -import { StockNotifyService } from './stock-notify.service'; - -@Module({ - controllers: [StockNotifyController], - providers: [StockNotifyService], - exports: [StockNotifyService], -}) -export class StockNotifyModule {} diff --git a/src/stock-notify/stock-notify.service.ts b/src/stock-notify/stock-notify.service.ts deleted file mode 100644 index 33c4508..0000000 --- a/src/stock-notify/stock-notify.service.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { PrismaService } from '../prisma/prisma.service'; - -@Injectable() -export class StockNotifyService { - constructor(private readonly prisma: PrismaService) {} - - async listForCustomer(customerId: string) { - const rows = await this.prisma.stockNotifyRequest.findMany({ - where: { customerId }, - orderBy: { createdAt: 'desc' }, - include: { - product: { - select: { - id: true, - name: true, - code: true, - color: true, - stock: true, - imageUrlsJson: true, - }, - }, - }, - }); - return rows.map((r) => this.mapCustomer(r)); - } - - async listAdmin(activeOnly = true) { - const rows = await this.prisma.stockNotifyRequest.findMany({ - where: activeOnly ? { active: true } : undefined, - orderBy: { createdAt: 'desc' }, - include: { - customer: { select: { firstName: true, lastName: true, phone: true } }, - product: { select: { name: true, code: true, color: true, stock: true } }, - }, - }); - return rows.map((r) => this.map(r)); - } - - async subscribe(customerId: string, productId: string) { - const product = await this.prisma.product.findUnique({ where: { id: productId } }); - if (!product) throw new NotFoundException('محصول یافت نشد'); - const row = await this.prisma.stockNotifyRequest.upsert({ - where: { customerId_productId: { customerId, productId } }, - create: { customerId, productId, active: true }, - update: { active: true, notifiedAt: null }, - include: { - customer: { select: { firstName: true, lastName: true, phone: true } }, - product: { select: { name: true, code: true, color: true, stock: true } }, - }, - }); - return this.map(row); - } - - async unsubscribe(customerId: string, productId: string) { - await this.prisma.stockNotifyRequest.updateMany({ - where: { customerId, productId }, - data: { active: false }, - }); - return { productId, active: false }; - } - - async markNotified(id: string) { - const existing = await this.prisma.stockNotifyRequest.findUnique({ - where: { id }, - }); - if (!existing) throw new NotFoundException('درخواست یافت نشد'); - const row = await this.prisma.stockNotifyRequest.update({ - where: { id }, - data: { notifiedAt: new Date(), active: false }, - include: { - customer: { select: { firstName: true, lastName: true, phone: true } }, - product: { select: { name: true, code: true, color: true, stock: true } }, - }, - }); - - await this.prisma.customerNotification.create({ - data: { - customerId: row.customerId, - event: 'stock_available', - title: 'موجودی محصول', - body: `محصول ${row.product.name} دوباره موجود شد.`, - tone: 'success', - link: `/product?id=${row.productId}`, - }, - }); - - return this.map(row); - } - - private productThumb(imageUrlsJson: unknown): string { - if (!Array.isArray(imageUrlsJson)) return ''; - const url = imageUrlsJson.find( - (item): item is string => typeof item === 'string' && item.trim().length > 0, - ); - return url?.trim() ?? ''; - } - - private mapCustomer(r: { - id: string; - productId: string; - active: boolean; - createdAt: Date; - notifiedAt: Date | null; - product: { - id: string; - name: string; - code: string; - color: string; - stock: number | null; - imageUrlsJson: unknown; - }; - }) { - return { - id: r.id, - productId: r.productId, - productName: r.product.name, - productCode: r.product.code, - catalogCode: r.product.code, - color: r.product.color, - stock: r.product.stock, - imageUrl: this.productThumb(r.product.imageUrlsJson), - active: r.active, - createdAt: r.createdAt.toISOString(), - notifiedAt: r.notifiedAt?.toISOString() ?? null, - }; - } - - private map(r: { - id: string; - customerId: string; - productId: string; - active: boolean; - createdAt: Date; - notifiedAt: Date | null; - customer: { firstName: string; lastName: string; phone: string }; - product: { name: string; code: string; color: string; stock: number | null }; - }) { - return { - id: r.id, - customerId: r.customerId, - customerName: `${r.customer.firstName} ${r.customer.lastName}`.trim(), - customerPhone: r.customer.phone, - productId: r.productId, - productName: r.product.name, - productCode: r.product.code, - color: r.product.color, - stock: r.product.stock, - active: r.active, - createdAt: r.createdAt.toISOString(), - notifiedAt: r.notifiedAt?.toISOString(), - }; - } -}