change nademitex2
This commit is contained in:
@@ -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([
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user