feat(goods): support merged secondary origin goods in create/update/detail

This commit is contained in:
yeuimu
2026-08-27 18:15:56 +08:00
parent 848eed0b6f
commit 9c279ae393
6 changed files with 729 additions and 418 deletions
@@ -15,6 +15,13 @@ export class BatchCreateItemDto {
@Min(1) @Min(1)
originGoodId!: number; originGoodId!: number;
@ApiProperty({ required: false, nullable: true, type: [Number], description: '副源原产品 ID,不含主源' })
@IsOptional()
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
mergedOriginGoodIds?: number[];
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsInt() @IsInt()
@@ -20,6 +20,13 @@ export class CreateGoodDto {
@Min(1) @Min(1)
originGoodId!: number; originGoodId!: number;
@ApiProperty({ required: false, nullable: true, type: [Number], description: '副源原产品 ID,不含主源' })
@IsOptional()
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
mergedOriginGoodIds?: number[];
@ApiProperty() @ApiProperty()
@IsInt() @IsInt()
@Min(1) @Min(1)
+146 -80
View File
@@ -6,33 +6,58 @@ export interface GoodRelations {
category?: { id: bigint; categoryName: string; categoryIcon: string | null } | null; category?: { id: bigint; categoryName: string; categoryIcon: string | null } | null;
tag?: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null } | null; tag?: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null } | null;
position?: { id: bigint; indexVal: number } | null; position?: { id: bigint; indexVal: number } | null;
originGood?: { originGood?: {
id: bigint; id: bigint;
sdsGoodId: string; sdsGoodId: string;
source: 'SDS' | 'CUSTOM'; source: 'SDS' | 'CUSTOM';
goodName: string | null; goodName: string | null;
goodImage: string | null; goodImage: string | null;
goodPrice: unknown; goodPrice: unknown;
detail?: { detail?: {
productCode: string | null; productCode: string | null;
syncedAt: Date; syncedAt: Date;
sizeChart: unknown; sizeChart: unknown;
packageSpecs: unknown; packageSpecs: unknown;
[key: string]: unknown; [key: string]: unknown;
} | null; } | null;
variants?: Array<{ variants?: Array<{
sdsVariantId: string; sdsVariantId: string;
sku: string; sku: string;
sizeName: string | null; sizeName: string | null;
colorName: string | null; colorName: string | null;
colorHex: string | null; colorHex: string | null;
price: unknown; price: unknown;
enabled: boolean; enabled: boolean;
[key: string]: unknown; [key: string]: unknown;
}>; }>;
_count?: { variants: number }; _count?: { variants: number };
} | null; } | null;
goodTags?: { tag: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null } }[]; goodTags?: { tag: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null } }[];
mergedOriginGoods?: Array<{
originGood: {
id: bigint;
sdsGoodId: string;
source: 'SDS' | 'CUSTOM';
goodName: string | null;
goodImage: string | null;
goodPrice: unknown;
detail?: { syncedAt: Date } | Record<string, unknown> | null;
variants?: Array<{ [key: string]: unknown }>;
_count?: { variants: number };
};
}>;
}
export interface MergedOriginGoodSummary {
id: string;
sdsGoodId: string;
source: 'SDS' | 'CUSTOM';
isCustom: boolean;
goodName: string | null;
goodImage: string | null;
goodPrice: string | null;
hasDetail: boolean;
variantCount: number;
} }
export class GoodDto { export class GoodDto {
@@ -81,24 +106,27 @@ export class GoodDto {
@ApiProperty({ required: false, type: Array }) @ApiProperty({ required: false, type: Array })
tags!: Array<{ id: string; tagName: string; tagColor: string | null; tagFontColor: string | null }>; tags!: Array<{ id: string; tagName: string; tagColor: string | null; tagFontColor: string | null }>;
@ApiProperty({ required: false, type: Array })
mergedOriginGoods!: MergedOriginGoodSummary[];
@ApiProperty({ required: false, nullable: true }) @ApiProperty({ required: false, nullable: true })
position?: { id: string; indexVal: number } | null; position?: { id: string; indexVal: number } | null;
@ApiProperty({ required: false, nullable: true }) @ApiProperty({ required: false, nullable: true })
originGood?: { originGood?: {
id: string; id: string;
sdsGoodId: string; sdsGoodId: string;
source: 'SDS' | 'CUSTOM'; source: 'SDS' | 'CUSTOM';
isCustom: boolean; isCustom: boolean;
goodName: string | null; goodName: string | null;
goodImage: string | null; goodImage: string | null;
goodPrice: string | null; goodPrice: string | null;
hasDetail: boolean; hasDetail: boolean;
detailSyncedAt: string | null; detailSyncedAt: string | null;
variantCount: number; variantCount: number;
sizeRowCount: number; sizeRowCount: number;
packageRowCount: number; packageRowCount: number;
productCode: string | null; productCode: string | null;
} | null; } | null;
static from( static from(
@@ -147,70 +175,108 @@ export class GoodDto {
tagFontColor: gt.tag.tagFontColor, tagFontColor: gt.tag.tagFontColor,
})) }))
: [], : [],
mergedOriginGoods: (rel.mergedOriginGoods ?? []).map((m) => ({
id: m.originGood.id.toString(),
sdsGoodId: m.originGood.sdsGoodId,
source: m.originGood.source,
isCustom: m.originGood.source === 'CUSTOM',
goodName: m.originGood.goodName,
goodImage: m.originGood.goodImage,
goodPrice:
m.originGood.goodPrice === null || m.originGood.goodPrice === undefined
? null
: (m.originGood.goodPrice as { toString(): string }).toString(),
hasDetail: Boolean(m.originGood.detail),
variantCount:
m.originGood._count?.variants ?? m.originGood.variants?.length ?? 0,
})),
position: rel.position position: rel.position
? { ? {
id: rel.position.id.toString(), id: rel.position.id.toString(),
indexVal: rel.position.indexVal, indexVal: rel.position.indexVal,
} }
: null, : null,
originGood: rel.originGood originGood: rel.originGood
? { ? {
id: rel.originGood.id.toString(), id: rel.originGood.id.toString(),
sdsGoodId: rel.originGood.sdsGoodId, sdsGoodId: rel.originGood.sdsGoodId,
source: rel.originGood.source, source: rel.originGood.source,
isCustom: rel.originGood.source === 'CUSTOM', isCustom: rel.originGood.source === 'CUSTOM',
goodName: rel.originGood.goodName, goodName: rel.originGood.goodName,
goodImage: rel.originGood.goodImage, goodImage: rel.originGood.goodImage,
goodPrice: goodPrice:
rel.originGood.goodPrice === null || rel.originGood.goodPrice === null ||
rel.originGood.goodPrice === undefined rel.originGood.goodPrice === undefined
? null ? null
: (rel.originGood.goodPrice as { toString(): string }).toString(), : (rel.originGood.goodPrice as { toString(): string }).toString(),
hasDetail: Boolean(rel.originGood.detail), hasDetail: Boolean(rel.originGood.detail),
detailSyncedAt: rel.originGood.detail?.syncedAt.toISOString() ?? null, detailSyncedAt: rel.originGood.detail?.syncedAt.toISOString() ?? null,
variantCount: rel.originGood._count?.variants ?? rel.originGood.variants?.length ?? 0, variantCount: rel.originGood._count?.variants ?? rel.originGood.variants?.length ?? 0,
sizeRowCount: GoodDto.jsonRows(rel.originGood.detail?.sizeChart), sizeRowCount: GoodDto.jsonRows(rel.originGood.detail?.sizeChart),
packageRowCount: GoodDto.jsonRows(rel.originGood.detail?.packageSpecs), packageRowCount: GoodDto.jsonRows(rel.originGood.detail?.packageSpecs),
productCode: rel.originGood.detail?.productCode ?? null, productCode: rel.originGood.detail?.productCode ?? null,
} }
: null, : null,
}; };
} }
private static jsonRows(value: unknown): number { private static jsonRows(value: unknown): number {
if (!value || typeof value !== 'object' || !('rows' in value)) return 0; if (!value || typeof value !== 'object' || !('rows' in value)) return 0;
const rows = (value as { rows?: unknown }).rows; const rows = (value as { rows?: unknown }).rows;
return Array.isArray(rows) ? rows.length : 0; return Array.isArray(rows) ? rows.length : 0;
} }
} }
export class GoodDetailDto extends GoodDto { export class GoodDetailDto extends GoodDto {
@ApiProperty({ nullable: true, type: Object }) @ApiProperty({ nullable: true, type: Object })
originDetail!: Record<string, unknown> | null; originDetail!: Record<string, unknown> | null;
@ApiProperty({ type: Array }) @ApiProperty({ type: Array })
variants!: Array<Record<string, unknown>>; variants!: Array<Record<string, unknown>>;
static fromGood(good: PrismaGood, rel: GoodRelations): GoodDetailDto { static fromGood(good: PrismaGood, rel: GoodRelations): GoodDetailDto {
const base = GoodDto.from(good, rel); const base = GoodDto.from(good, rel);
const detail = rel.originGood?.detail; const detail = rel.originGood?.detail;
return { const toAnnotated = (
...base, variant: Record<string, unknown>,
originDetail: detail ? { ...detail, syncedAt: detail.syncedAt.toISOString() } : null, originGoodId: string,
variants: (rel.originGood?.variants ?? []).map((variant) => ({ originGoodName: string | null,
...variant, ) => ({
price: ...variant,
variant.price === null || variant.price === undefined price:
? null variant.price === null || variant.price === undefined
: (variant.price as { toString(): string }).toString(), ? null
})), : (variant.price as unknown as { toString(): string }).toString(),
}; originGoodId,
} originGoodName,
} });
const primaryId = good.originGoodId.toString();
const primaryName = rel.originGood?.goodName ?? null;
const mergedVariants = [
...(rel.originGood?.variants ?? []).map((variant) =>
toAnnotated(variant as Record<string, unknown>, primaryId, primaryName),
),
...(rel.mergedOriginGoods ?? []).flatMap((m) =>
(m.originGood.variants ?? []).map((variant) =>
toAnnotated(
variant,
m.originGood.id.toString(),
m.originGood.goodName,
),
),
),
];
return {
...base,
originDetail: detail ? { ...detail, syncedAt: detail.syncedAt.toISOString() } : null,
variants: mergedVariants,
};
}
}
export interface PaginatedGoods { export interface PaginatedGoods {
items: GoodDto[]; items: GoodDto[];
total: number; total: number;
page: number; page: number;
pageSize: number; pageSize: number;
} }
@@ -19,6 +19,13 @@ export class UpdateGoodDto {
@Min(1) @Min(1)
originGoodId?: number; originGoodId?: number;
@ApiProperty({ required: false, nullable: true, type: [Number], description: '副源原产品 ID 全量覆盖,不含主源' })
@IsOptional()
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
mergedOriginGoodIds?: number[];
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsInt() @IsInt()
+186 -57
View File
@@ -4,8 +4,8 @@ import {
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { GoodsService } from './goods.service'; import { GoodsService } from './goods.service';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { SyncService } from '../sync/sync.service'; import { SyncService } from '../sync/sync.service';
describe('GoodsService', () => { describe('GoodsService', () => {
let service: GoodsService; let service: GoodsService;
@@ -23,14 +23,14 @@ describe('GoodsService', () => {
beforeAll(async () => { beforeAll(async () => {
const moduleRef = await Test.createTestingModule({ const moduleRef = await Test.createTestingModule({
providers: [ providers: [
GoodsService, GoodsService,
PrismaService, PrismaService,
{ {
provide: SyncService, provide: SyncService,
useValue: { queueProductDetailSync: jest.fn() }, useValue: { queueProductDetailSync: jest.fn() },
}, },
], ],
}).compile(); }).compile();
service = moduleRef.get(GoodsService); service = moduleRef.get(GoodsService);
prisma = moduleRef.get(PrismaService); prisma = moduleRef.get(PrismaService);
@@ -101,7 +101,7 @@ describe('GoodsService', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
it('creates and reads back a good', async () => { it('creates and reads back a good', async () => {
const created = await service.create({ const created = await service.create({
goodName: `Goods Test ${stamp} basic`, goodName: `Goods Test ${stamp} basic`,
originGoodId: Number(originGoodIds[0]), originGoodId: Number(originGoodIds[0]),
@@ -117,52 +117,52 @@ describe('GoodsService', () => {
const fetched = await service.findOne(BigInt(created.id)); const fetched = await service.findOne(BigInt(created.id));
expect(fetched.goodName).toBe(`Goods Test ${stamp} basic`); expect(fetched.goodName).toBe(`Goods Test ${stamp} basic`);
}); });
it('creates, edits, and removes a fully editable custom good', async () => { it('creates, edits, and removes a fully editable custom good', async () => {
const created = await service.createCustom({ const created = await service.createCustom({
goodName: `Goods Test ${stamp} custom`, goodName: `Goods Test ${stamp} custom`,
goodImage: 'https://example.com/custom.png', goodImage: 'https://example.com/custom.png',
goodPrice: '29.90', goodPrice: '29.90',
countryId: Number(countryId), countryId: Number(countryId),
categoryId: Number(categoryId), categoryId: Number(categoryId),
tagIds: [Number(tagId)], tagIds: [Number(tagId)],
detail: { detail: {
productCode: `CUSTOM-${stamp}`, productCode: `CUSTOM-${stamp}`,
materialDescription: 'Cotton', materialDescription: 'Cotton',
sizeChart: { columns: [], rows: [] }, sizeChart: { columns: [], rows: [] },
packageSpecs: { rows: [] }, packageSpecs: { rows: [] },
}, },
variants: [ variants: [
{ sku: `CUSTOM-SKU-${stamp}`, sizeName: 'S', price: '29.90' }, { sku: `CUSTOM-SKU-${stamp}`, sizeName: 'S', price: '29.90' },
], ],
}); });
expect(created.originGood?.source).toBe('CUSTOM'); expect(created.originGood?.source).toBe('CUSTOM');
expect(created.originGood?.isCustom).toBe(true); expect(created.originGood?.isCustom).toBe(true);
expect(created.originGood?.goodPrice).toBe('29.9'); expect(created.originGood?.goodPrice).toBe('29.9');
expect(created.variants).toHaveLength(1); expect(created.variants).toHaveLength(1);
const updated = await service.updateCustomContent(BigInt(created.id), { const updated = await service.updateCustomContent(BigInt(created.id), {
goodName: `Goods Test ${stamp} custom edited`, goodName: `Goods Test ${stamp} custom edited`,
goodPrice: '39.90', goodPrice: '39.90',
detail: { materialDescription: 'Organic cotton' }, detail: { materialDescription: 'Organic cotton' },
variants: [ variants: [
{ sku: `CUSTOM-SKU-${stamp}-M`, sizeName: 'M', price: '39.90' }, { sku: `CUSTOM-SKU-${stamp}-M`, sizeName: 'M', price: '39.90' },
], ],
}); });
expect(updated.goodName).toContain('custom edited'); expect(updated.goodName).toContain('custom edited');
expect(updated.originGood?.goodPrice).toBe('39.9'); expect(updated.originGood?.goodPrice).toBe('39.9');
expect(updated.originDetail?.materialDescription).toBe('Organic cotton'); expect(updated.originDetail?.materialDescription).toBe('Organic cotton');
expect(updated.originDetail?.productCode).toBe(`CUSTOM-${stamp}`); expect(updated.originDetail?.productCode).toBe(`CUSTOM-${stamp}`);
expect(updated.variants[0]?.sizeName).toBe('M'); expect(updated.variants[0]?.sizeName).toBe('M');
const customOriginId = BigInt(updated.originGoodId); const customOriginId = BigInt(updated.originGoodId);
await service.remove(BigInt(updated.id)); await service.remove(BigInt(updated.id));
await expect( await expect(
prisma.originGood.findUnique({ where: { id: customOriginId } }), prisma.originGood.findUnique({ where: { id: customOriginId } }),
).resolves.toBeNull(); ).resolves.toBeNull();
}); });
it('filters by countryId, tagId, positionId and keyword', async () => { it('filters by countryId, tagId, positionId and keyword', async () => {
const result = await service.findAll({ const result = await service.findAll({
@@ -261,6 +261,135 @@ describe('GoodsService', () => {
expect(after.total).toBe(before.total); expect(after.total).toBe(before.total);
}); });
describe('merged origin goods', () => {
it('creates a good with merged origin goods and reads them back', async () => {
const created = await service.create({
goodName: `Goods Test ${stamp} merged`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[2]), Number(originGoodIds[3])],
countryId: Number(countryId),
categoryId: Number(categoryId),
});
expect(created.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
[originGoodIds[2].toString(), originGoodIds[3].toString()].sort(),
);
const fetched = await service.findOne(BigInt(created.id));
expect(fetched.mergedOriginGoods.length).toBe(2);
});
it('rejects mergedOriginGoodIds containing the primary', async () => {
await expect(
service.create({
goodName: `Goods Test ${stamp} bad-primary`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[1])],
countryId: Number(countryId),
categoryId: Number(categoryId),
}),
).rejects.toThrow(BadRequestException);
});
it('rejects mergedOriginGoodIds that do not exist', async () => {
await expect(
service.create({
goodName: `Goods Test ${stamp} bad-missing`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [999999999],
countryId: Number(countryId),
categoryId: Number(categoryId),
}),
).rejects.toThrow(BadRequestException);
});
it('replaces merged origin goods on update', async () => {
const created = await service.create({
goodName: `Goods Test ${stamp} replace`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[2])],
countryId: Number(countryId),
categoryId: Number(categoryId),
});
const updated = await service.update(BigInt(created.id), {
mergedOriginGoodIds: [Number(originGoodIds[3]), Number(originGoodIds[4])],
});
expect(updated.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
[originGoodIds[3].toString(), originGoodIds[4].toString()].sort(),
);
});
it('moves old primary into merged list when switching primary', async () => {
const created = await service.create({
goodName: `Goods Test ${stamp} switch`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[2])],
countryId: Number(countryId),
categoryId: Number(categoryId),
});
const updated = await service.update(BigInt(created.id), {
originGoodId: Number(originGoodIds[2]),
mergedOriginGoodIds: [Number(originGoodIds[1]), Number(originGoodIds[3])],
});
expect(updated.originGoodId).toBe(originGoodIds[2].toString());
expect(updated.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
[originGoodIds[1].toString(), originGoodIds[3].toString()].sort(),
);
});
it('cascades merged rows on good removal', async () => {
const created = await service.create({
goodName: `Goods Test ${stamp} cascade`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[2])],
countryId: Number(countryId),
categoryId: Number(categoryId),
});
await service.remove(BigInt(created.id));
const rows = await prisma.goodOriginGood.count({
where: { goodId: BigInt(created.id) },
});
expect(rows).toBe(0);
});
it('returns merged variants with source annotation in detail', async () => {
const v1 = await prisma.originGoodVariant.create({
data: {
originGoodId: originGoodIds[1],
sdsVariantId: `mv-pri-${stamp}`,
sku: `MV-PRI-${stamp}`,
colorName: '黑色',
},
});
const v2 = await prisma.originGoodVariant.create({
data: {
originGoodId: originGoodIds[2],
sdsVariantId: `mv-sec-${stamp}`,
sku: `MV-SEC-${stamp}`,
colorName: '白色',
},
});
try {
const created = await service.create({
goodName: `Goods Test ${stamp} variants`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[2])],
countryId: Number(countryId),
categoryId: Number(categoryId),
});
const detail = await service.findOne(BigInt(created.id));
const sources = new Set(
detail.variants.map((v) => v['originGoodId'] as string),
);
expect(sources.has(originGoodIds[1].toString())).toBe(true);
expect(sources.has(originGoodIds[2].toString())).toBe(true);
expect(detail.variants).toHaveLength(2);
expect(detail.mergedOriginGoods.find((m) => m.id === originGoodIds[2].toString())?.variantCount).toBe(1);
} finally {
await prisma.originGoodVariant.delete({ where: { id: v1.id } });
await prisma.originGoodVariant.delete({ where: { id: v2.id } });
}
});
});
it('throws NotFoundException for unknown id', async () => { it('throws NotFoundException for unknown id', async () => {
await expect(service.findOne(BigInt(99999999))).rejects.toBeInstanceOf( await expect(service.findOne(BigInt(99999999))).rejects.toBeInstanceOf(
NotFoundException, NotFoundException,
+376 -281
View File
@@ -9,38 +9,50 @@ import { CreateGoodDto } from './dto/create-good.dto';
import { UpdateGoodDto } from './dto/update-good.dto'; import { UpdateGoodDto } from './dto/update-good.dto';
import { QueryGoodDto } from './dto/query-good.dto'; import { QueryGoodDto } from './dto/query-good.dto';
import { BatchCreateGoodDto } from './dto/batch-create-good.dto'; import { BatchCreateGoodDto } from './dto/batch-create-good.dto';
import { BatchPriorityDto } from './dto/batch-priority.dto'; import { BatchPriorityDto } from './dto/batch-priority.dto';
import { GoodDetailDto, GoodDto, PaginatedGoods } from './dto/good.dto'; import { GoodDetailDto, GoodDto, PaginatedGoods } from './dto/good.dto';
import { SyncService } from '../sync/sync.service'; import { SyncService } from '../sync/sync.service';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { import {
CreateCustomGoodDto, CreateCustomGoodDto,
CustomGoodDetailDto, CustomGoodDetailDto,
CustomGoodVariantDto, CustomGoodVariantDto,
UpdateCustomGoodContentDto, UpdateCustomGoodContentDto,
} from './dto/custom-good.dto'; } from './dto/custom-good.dto';
const GOOD_INCLUDE = { const GOOD_INCLUDE = {
country: true, country: true,
category: true, category: true,
tag: true, tag: true,
position: true, position: true,
originGood: { originGood: {
include: { include: {
detail: true, detail: true,
variants: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] }, variants: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] },
_count: { select: { variants: true } }, _count: { select: { variants: true } },
}, },
}, },
goodTags: { include: { tag: true } }, goodTags: { include: { tag: true } },
mergedOriginGoods: {
orderBy: { createdAt: 'asc' },
include: {
originGood: {
include: {
detail: true,
variants: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] },
_count: { select: { variants: true } },
},
},
},
},
} satisfies Prisma.GoodInclude; } satisfies Prisma.GoodInclude;
@Injectable() @Injectable()
export class GoodsService { export class GoodsService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly syncService: SyncService, private readonly syncService: SyncService,
) {} ) {}
async findAll(query: QueryGoodDto): Promise<PaginatedGoods> { async findAll(query: QueryGoodDto): Promise<PaginatedGoods> {
const { page, pageSize, countryId, categoryId, tagId, positionId, keyword } = query; const { page, pageSize, countryId, categoryId, tagId, positionId, keyword } = query;
@@ -75,6 +87,7 @@ export class GoodsService {
position: g.position, position: g.position,
originGood: g.originGood, originGood: g.originGood,
goodTags: g.goodTags, goodTags: g.goodTags,
mergedOriginGoods: g.mergedOriginGoods,
})), })),
total, total,
page, page,
@@ -82,25 +95,31 @@ export class GoodsService {
}; };
} }
async findOne(id: bigint): Promise<GoodDetailDto> { async findOne(id: bigint): Promise<GoodDetailDto> {
const good = await this.prisma.good.findUnique({ const good = await this.prisma.good.findUnique({
where: { id }, where: { id },
include: GOOD_INCLUDE, include: GOOD_INCLUDE,
}); });
if (!good) throw new NotFoundException(`Good ${id} not found`); if (!good) throw new NotFoundException(`Good ${id} not found`);
return GoodDetailDto.fromGood(good, { return GoodDetailDto.fromGood(good, {
country: good.country, country: good.country,
category: good.category, category: good.category,
tag: good.tag, tag: good.tag,
position: good.position, position: good.position,
originGood: good.originGood, originGood: good.originGood,
goodTags: good.goodTags, goodTags: good.goodTags,
mergedOriginGoods: good.mergedOriginGoods,
}); });
} }
async create(dto: CreateGoodDto): Promise<GoodDto> { async create(dto: CreateGoodDto): Promise<GoodDto> {
await this.ensureReferences(dto); await this.ensureReferences(dto);
const result = await this.prisma.$transaction(async (tx) => { const mergedIds = this.dedupeMergedIds(
BigInt(dto.originGoodId),
dto.mergedOriginGoodIds,
);
await this.ensureMergedOriginGoods(mergedIds);
const result = await this.prisma.$transaction(async (tx) => {
const created = await tx.good.create({ const created = await tx.good.create({
data: { data: {
goodName: dto.goodName, goodName: dto.goodName,
@@ -120,132 +139,152 @@ export class GoodsService {
})), })),
}); });
} }
if (mergedIds.length > 0) {
await tx.goodOriginGood.createMany({
data: mergedIds.map((originGoodId) => ({
goodId: created.id,
originGoodId,
})),
});
}
const result = await tx.good.findUniqueOrThrow({ const result = await tx.good.findUniqueOrThrow({
where: { id: created.id }, where: { id: created.id },
include: GOOD_INCLUDE, include: GOOD_INCLUDE,
}); });
return GoodDto.from(result, { return GoodDto.from(result, {
country: result.country, country: result.country,
category: result.category, category: result.category,
tag: result.tag, tag: result.tag,
position: result.position, position: result.position,
originGood: result.originGood, originGood: result.originGood,
goodTags: result.goodTags, goodTags: result.goodTags,
}); mergedOriginGoods: result.mergedOriginGoods,
}); });
if ( });
result.originGood?.source === 'SDS' && if (
result.originGood.sdsGoodId && result.originGood?.source === 'SDS' &&
!result.originGood.hasDetail result.originGood.sdsGoodId &&
) { !result.originGood.hasDetail
this.syncService.queueProductDetailSync(result.originGood.sdsGoodId); ) {
} this.syncService.queueProductDetailSync(result.originGood.sdsGoodId);
return result; }
} return result;
}
async createCustom(dto: CreateCustomGoodDto): Promise<GoodDetailDto> {
await this.ensureCountry(dto.countryId); async createCustom(dto: CreateCustomGoodDto): Promise<GoodDetailDto> {
await this.ensureCategory(dto.categoryId); await this.ensureCountry(dto.countryId);
if (dto.positionId !== undefined) await this.ensurePosition(dto.positionId); await this.ensureCategory(dto.categoryId);
for (const tagId of dto.tagIds ?? []) await this.ensureTag(tagId); if (dto.positionId !== undefined) await this.ensurePosition(dto.positionId);
for (const tagId of dto.tagIds ?? []) await this.ensureTag(tagId);
const goodId = await this.prisma.$transaction(async (tx) => {
const originGood = await tx.originGood.create({ const goodId = await this.prisma.$transaction(async (tx) => {
data: { const originGood = await tx.originGood.create({
source: 'CUSTOM', data: {
sdsGoodId: `custom-${randomUUID()}`, source: 'CUSTOM',
goodName: dto.goodName, sdsGoodId: `custom-${randomUUID()}`,
goodImage: dto.goodImage ?? null, goodName: dto.goodName,
goodPrice: this.decimal(dto.goodPrice), goodImage: dto.goodImage ?? null,
detail: { goodPrice: this.decimal(dto.goodPrice),
create: this.customDetailData( detail: {
dto.detail ?? {}, create: this.customDetailData(
) as Prisma.OriginGoodDetailUncheckedCreateWithoutOriginGoodInput, dto.detail ?? {},
}, ) as Prisma.OriginGoodDetailUncheckedCreateWithoutOriginGoodInput,
}, },
}); },
if (dto.variants?.length) { });
await this.replaceCustomVariants(tx, originGood.id, dto.variants); if (dto.variants?.length) {
} await this.replaceCustomVariants(tx, originGood.id, dto.variants);
const good = await tx.good.create({ }
data: { const good = await tx.good.create({
originGoodId: originGood.id, data: {
countryId: BigInt(dto.countryId), originGoodId: originGood.id,
categoryId: BigInt(dto.categoryId), countryId: BigInt(dto.countryId),
positionId: categoryId: BigInt(dto.categoryId),
dto.positionId === undefined ? null : BigInt(dto.positionId), positionId:
goodName: dto.goodName, dto.positionId === undefined ? null : BigInt(dto.positionId),
goodImage: dto.goodImage ?? null, goodName: dto.goodName,
goodPriority: dto.goodPriority ?? 0, goodImage: dto.goodImage ?? null,
}, goodPriority: dto.goodPriority ?? 0,
}); },
if (dto.tagIds?.length) { });
await tx.goodTag.createMany({ if (dto.tagIds?.length) {
data: dto.tagIds.map((tagId) => ({ await tx.goodTag.createMany({
goodId: good.id, data: dto.tagIds.map((tagId) => ({
tagId: BigInt(tagId), goodId: good.id,
})), tagId: BigInt(tagId),
}); })),
} });
return good.id; }
}); return good.id;
return this.findOne(goodId); });
} return this.findOne(goodId);
}
async updateCustomContent(
id: bigint, async updateCustomContent(
dto: UpdateCustomGoodContentDto, id: bigint,
): Promise<GoodDetailDto> { dto: UpdateCustomGoodContentDto,
const existing = await this.prisma.good.findUnique({ ): Promise<GoodDetailDto> {
where: { id }, const existing = await this.prisma.good.findUnique({
include: { originGood: true }, where: { id },
}); include: { originGood: true },
if (!existing) throw new NotFoundException(`Good ${id} not found`); });
if (existing.originGood.source !== 'CUSTOM') { if (!existing) throw new NotFoundException(`Good ${id} not found`);
throw new BadRequestException('SDS 映射商品的上游信息不可修改'); if (existing.originGood.source !== 'CUSTOM') {
} throw new BadRequestException('SDS 映射商品的上游信息不可修改');
}
await this.prisma.$transaction(async (tx) => {
await tx.originGood.update({ await this.prisma.$transaction(async (tx) => {
where: { id: existing.originGoodId }, await tx.originGood.update({
data: { where: { id: existing.originGoodId },
goodName: dto.goodName, data: {
goodImage: dto.goodImage, goodName: dto.goodName,
goodPrice: goodImage: dto.goodImage,
dto.goodPrice === undefined ? undefined : this.decimal(dto.goodPrice), goodPrice:
}, dto.goodPrice === undefined ? undefined : this.decimal(dto.goodPrice),
}); },
if (dto.detail !== undefined) { });
await tx.originGoodDetail.upsert({ if (dto.detail !== undefined) {
where: { originGoodId: existing.originGoodId }, await tx.originGoodDetail.upsert({
create: { where: { originGoodId: existing.originGoodId },
originGoodId: existing.originGoodId, create: {
...(this.customDetailData( originGoodId: existing.originGoodId,
dto.detail, ...(this.customDetailData(
) as Prisma.OriginGoodDetailUncheckedCreateWithoutOriginGoodInput), dto.detail,
}, ) as Prisma.OriginGoodDetailUncheckedCreateWithoutOriginGoodInput),
update: this.customDetailData(dto.detail, true), },
}); update: this.customDetailData(dto.detail, true),
} });
if (dto.variants !== undefined) { }
await this.replaceCustomVariants( if (dto.variants !== undefined) {
tx, await this.replaceCustomVariants(
existing.originGoodId, tx,
dto.variants, existing.originGoodId,
); dto.variants,
} );
const goodData: Prisma.GoodUpdateInput = {}; }
if (dto.goodName !== undefined) goodData.goodName = dto.goodName; const goodData: Prisma.GoodUpdateInput = {};
if (dto.goodImage !== undefined) goodData.goodImage = dto.goodImage; if (dto.goodName !== undefined) goodData.goodName = dto.goodName;
if (Object.keys(goodData).length) { if (dto.goodImage !== undefined) goodData.goodImage = dto.goodImage;
await tx.good.update({ where: { id }, data: goodData }); if (Object.keys(goodData).length) {
} await tx.good.update({ where: { id }, data: goodData });
}); }
return this.findOne(id); });
} return this.findOne(id);
}
async update(id: bigint, dto: UpdateGoodDto): Promise<GoodDto> { async update(id: bigint, dto: UpdateGoodDto): Promise<GoodDto> {
await this.findOne(id); await this.findOne(id);
let mergedIds: bigint[] | undefined;
if (dto.mergedOriginGoodIds !== undefined || dto.originGoodId !== undefined) {
const current = await this.prisma.good.findUniqueOrThrow({
where: { id },
select: { originGoodId: true },
});
const primaryId =
dto.originGoodId !== undefined ? BigInt(dto.originGoodId) : current.originGoodId;
mergedIds = this.dedupeMergedIds(primaryId, dto.mergedOriginGoodIds);
await this.ensureMergedOriginGoods(mergedIds);
}
const data: Prisma.GoodUpdateInput = {}; const data: Prisma.GoodUpdateInput = {};
if (dto.goodName !== undefined) data.goodName = dto.goodName; if (dto.goodName !== undefined) data.goodName = dto.goodName;
if (dto.originGoodId !== undefined) { if (dto.originGoodId !== undefined) {
@@ -274,7 +313,7 @@ export class GoodsService {
} }
} }
const result = await this.prisma.$transaction(async (tx) => { const result = await this.prisma.$transaction(async (tx) => {
if (dto.tagIds !== undefined) { if (dto.tagIds !== undefined) {
await tx.goodTag.deleteMany({ where: { goodId: id } }); await tx.goodTag.deleteMany({ where: { goodId: id } });
if (dto.tagIds.length > 0) { if (dto.tagIds.length > 0) {
@@ -286,47 +325,59 @@ export class GoodsService {
}); });
} }
} }
if (mergedIds !== undefined) {
await tx.goodOriginGood.deleteMany({ where: { goodId: id } });
if (mergedIds.length > 0) {
await tx.goodOriginGood.createMany({
data: mergedIds.map((originGoodId) => ({
goodId: id,
originGoodId,
})),
});
}
}
const updated = await tx.good.update({ const updated = await tx.good.update({
where: { id }, where: { id },
data, data,
include: GOOD_INCLUDE, include: GOOD_INCLUDE,
}); });
return GoodDto.from(updated, { return GoodDto.from(updated, {
country: updated.country, country: updated.country,
category: updated.category, category: updated.category,
tag: updated.tag, tag: updated.tag,
position: updated.position, position: updated.position,
originGood: updated.originGood, originGood: updated.originGood,
goodTags: updated.goodTags, goodTags: updated.goodTags,
}); mergedOriginGoods: updated.mergedOriginGoods,
}); });
if ( });
result.originGood?.source === 'SDS' && if (
result.originGood.sdsGoodId && result.originGood?.source === 'SDS' &&
!result.originGood.hasDetail result.originGood.sdsGoodId &&
) { !result.originGood.hasDetail
this.syncService.queueProductDetailSync(result.originGood.sdsGoodId); ) {
} this.syncService.queueProductDetailSync(result.originGood.sdsGoodId);
return result; }
return result;
} }
async remove(id: bigint): Promise<{ id: string }> { async remove(id: bigint): Promise<{ id: string }> {
const good = await this.prisma.good.findUnique({ const good = await this.prisma.good.findUnique({
where: { id }, where: { id },
include: { originGood: true }, include: { originGood: true },
}); });
if (!good) throw new NotFoundException(`Good ${id} not found`); if (!good) throw new NotFoundException(`Good ${id} not found`);
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
await tx.good.delete({ where: { id } }); await tx.good.delete({ where: { id } });
if (good.originGood.source === 'CUSTOM') { if (good.originGood.source === 'CUSTOM') {
const remaining = await tx.good.count({ const remaining = await tx.good.count({
where: { originGoodId: good.originGoodId }, where: { originGoodId: good.originGoodId },
}); });
if (remaining === 0) { if (remaining === 0) {
await tx.originGood.delete({ where: { id: good.originGoodId } }); await tx.originGood.delete({ where: { id: good.originGoodId } });
} }
} }
}); });
return { id: id.toString() }; return { id: id.toString() };
} }
@@ -335,16 +386,16 @@ export class GoodsService {
* or none do. * or none do.
*/ */
async batchUpdatePriority(dto: BatchPriorityDto): Promise<{ count: number }> { async batchUpdatePriority(dto: BatchPriorityDto): Promise<{ count: number }> {
const result = await this.prisma.$transaction(async (tx) => { const result = await this.prisma.$transaction(async (tx) => {
for (const item of dto.items) { for (const item of dto.items) {
await tx.good.update({ await tx.good.update({
where: { id: BigInt(item.id) }, where: { id: BigInt(item.id) },
data: { goodPriority: item.priority }, data: { goodPriority: item.priority },
}); });
} }
return { count: dto.items.length }; return { count: dto.items.length };
}); });
return result; return result;
} }
/** /**
@@ -358,7 +409,7 @@ export class GoodsService {
await this.ensureTag(tagId); await this.ensureTag(tagId);
} }
} }
const result = await this.prisma.$transaction(async (tx) => { const result = await this.prisma.$transaction(async (tx) => {
const created: GoodDto[] = []; const created: GoodDto[] = [];
for (const item of dto.items) { for (const item of dto.items) {
const og = await tx.originGood.findUnique({ const og = await tx.originGood.findUnique({
@@ -388,6 +439,24 @@ export class GoodsService {
})), })),
}); });
} }
const itemMerged = this.dedupeMergedIds(og.id, item.mergedOriginGoodIds);
if (itemMerged.length > 0) {
const existRows = await tx.originGood.findMany({
where: { id: { in: itemMerged } },
select: { id: true },
});
if (existRows.length !== itemMerged.length) {
const found = new Set(existRows.map((r) => r.id.toString()));
const missing = itemMerged.find((mid) => !found.has(mid.toString()));
throw new BadRequestException(`Origin good ${missing} not found`);
}
await tx.goodOriginGood.createMany({
data: itemMerged.map((originGoodId) => ({
goodId: row.id,
originGoodId,
})),
});
}
const result = await tx.good.findUniqueOrThrow({ const result = await tx.good.findUniqueOrThrow({
where: { id: row.id }, where: { id: row.id },
include: GOOD_INCLUDE, include: GOOD_INCLUDE,
@@ -399,23 +468,24 @@ export class GoodsService {
position: result.position, position: result.position,
originGood: result.originGood, originGood: result.originGood,
goodTags: result.goodTags, goodTags: result.goodTags,
mergedOriginGoods: result.mergedOriginGoods,
})); }));
} }
return created; return created;
}); });
for (const goodId of new Set( for (const goodId of new Set(
result result
.filter( .filter(
(item) => (item) =>
item.originGood?.source === 'SDS' && item.originGood?.source === 'SDS' &&
!item.originGood.hasDetail, !item.originGood.hasDetail,
) )
.map((item) => item.originGood?.sdsGoodId) .map((item) => item.originGood?.sdsGoodId)
.filter((id): id is string => Boolean(id)), .filter((id): id is string => Boolean(id)),
)) { )) {
this.syncService.queueProductDetailSync(goodId); this.syncService.queueProductDetailSync(goodId);
} }
return result; return result;
} }
/** /**
@@ -446,6 +516,31 @@ export class GoodsService {
if (!og) throw new BadRequestException(`Origin good ${id} not found`); if (!og) throw new BadRequestException(`Origin good ${id} not found`);
} }
/** Dedupe merged ids and reject any that equals the primary source. */
private dedupeMergedIds(primaryId: bigint, ids?: number[]): bigint[] {
if (!ids || ids.length === 0) return [];
const unique = [...new Set(ids.map((id) => BigInt(id)))];
if (unique.includes(primaryId)) {
throw new BadRequestException(
'mergedOriginGoodIds 不能包含主源 originGoodId',
);
}
return unique;
}
private async ensureMergedOriginGoods(ids: bigint[]) {
if (ids.length === 0) return;
const rows = await this.prisma.originGood.findMany({
where: { id: { in: ids } },
select: { id: true },
});
if (rows.length !== ids.length) {
const found = new Set(rows.map((r) => r.id.toString()));
const missing = ids.find((id) => !found.has(id.toString()));
throw new BadRequestException(`Origin good ${missing} not found`);
}
}
private async ensureCountry(id: number) { private async ensureCountry(id: number) {
const c = await this.prisma.country.findUnique({ where: { id: BigInt(id) } }); const c = await this.prisma.country.findUnique({ where: { id: BigInt(id) } });
if (!c) throw new BadRequestException(`Country ${id} not found`); if (!c) throw new BadRequestException(`Country ${id} not found`);
@@ -456,12 +551,12 @@ export class GoodsService {
if (!c) throw new BadRequestException(`Category ${id} not found`); if (!c) throw new BadRequestException(`Category ${id} not found`);
} }
private async ensureTag(id: number) { private async ensureTag(id: number) {
const t = await this.prisma.tag.findUnique({ where: { id: BigInt(id) } }); const t = await this.prisma.tag.findUnique({ where: { id: BigInt(id) } });
if (!t) throw new BadRequestException(`Tag ${id} not found`); if (!t) throw new BadRequestException(`Tag ${id} not found`);
} }
private async ensureReferences(dto: CreateGoodDto) { private async ensureReferences(dto: CreateGoodDto) {
await this.ensureOriginGood(dto.originGoodId); await this.ensureOriginGood(dto.originGoodId);
await this.ensureCountry(dto.countryId); await this.ensureCountry(dto.countryId);
await this.ensureCategory(dto.categoryId); await this.ensureCategory(dto.categoryId);
@@ -474,94 +569,94 @@ export class GoodsService {
const p = await this.prisma.position.findUnique({ where: { id: BigInt(dto.positionId) } }); const p = await this.prisma.position.findUnique({ where: { id: BigInt(dto.positionId) } });
if (!p) throw new BadRequestException(`Position ${dto.positionId} not found`); if (!p) throw new BadRequestException(`Position ${dto.positionId} not found`);
} }
} }
private async ensurePosition(id: number) { private async ensurePosition(id: number) {
const position = await this.prisma.position.findUnique({ const position = await this.prisma.position.findUnique({
where: { id: BigInt(id) }, where: { id: BigInt(id) },
}); });
if (!position) throw new BadRequestException(`Position ${id} not found`); if (!position) throw new BadRequestException(`Position ${id} not found`);
} }
private decimal(value: string | null | undefined): Prisma.Decimal | null { private decimal(value: string | null | undefined): Prisma.Decimal | null {
return value === undefined || value === null || value === '' return value === undefined || value === null || value === ''
? null ? null
: new Prisma.Decimal(value); : new Prisma.Decimal(value);
} }
private customDetailData( private customDetailData(
detail: CustomGoodDetailDto, detail: CustomGoodDetailDto,
preserveMissing = false, preserveMissing = false,
): Prisma.OriginGoodDetailUncheckedUpdateInput { ): Prisma.OriginGoodDetailUncheckedUpdateInput {
const nullable = <T>(value: T | null | undefined): T | null | undefined => const nullable = <T>(value: T | null | undefined): T | null | undefined =>
preserveMissing && value === undefined ? undefined : value ?? null; preserveMissing && value === undefined ? undefined : value ?? null;
const decimal = (value: string | null | undefined) => const decimal = (value: string | null | undefined) =>
preserveMissing && value === undefined ? undefined : this.decimal(value); preserveMissing && value === undefined ? undefined : this.decimal(value);
const json = ( const json = (
value: Record<string, unknown> | null | undefined, value: Record<string, unknown> | null | undefined,
): Prisma.InputJsonValue | Prisma.NullTypes.DbNull | undefined => ): Prisma.InputJsonValue | Prisma.NullTypes.DbNull | undefined =>
preserveMissing && value === undefined preserveMissing && value === undefined
? undefined ? undefined
: value === null || value === undefined : value === null || value === undefined
? Prisma.DbNull ? Prisma.DbNull
: (value as Prisma.InputJsonValue); : (value as Prisma.InputJsonValue);
return { return {
productCode: nullable(detail.productCode), productCode: nullable(detail.productCode),
englishName: nullable(detail.englishName), englishName: nullable(detail.englishName),
blankDesignUrl: nullable(detail.blankDesignUrl), blankDesignUrl: nullable(detail.blankDesignUrl),
detailsPageVideoUrl: nullable(detail.detailsPageVideoUrl), detailsPageVideoUrl: nullable(detail.detailsPageVideoUrl),
textureName: nullable(detail.textureName), textureName: nullable(detail.textureName),
productionCycleHours: nullable(detail.productionCycleHours), productionCycleHours: nullable(detail.productionCycleHours),
minWeightG: decimal(detail.minWeightG), minWeightG: decimal(detail.minWeightG),
reminder: nullable(detail.reminder), reminder: nullable(detail.reminder),
productionProcess: nullable(detail.productionProcess), productionProcess: nullable(detail.productionProcess),
materialDescription: nullable(detail.materialDescription), materialDescription: nullable(detail.materialDescription),
productPerformance: nullable(detail.productPerformance), productPerformance: nullable(detail.productPerformance),
applicableScenarios: nullable(detail.applicableScenarios), applicableScenarios: nullable(detail.applicableScenarios),
washingInstructions: nullable(detail.washingInstructions), washingInstructions: nullable(detail.washingInstructions),
specialDescription: nullable(detail.specialDescription), specialDescription: nullable(detail.specialDescription),
designExplanation: nullable(detail.designExplanation), designExplanation: nullable(detail.designExplanation),
designArea: nullable(detail.designArea), designArea: nullable(detail.designArea),
pictureRequest: nullable(detail.pictureRequest), pictureRequest: nullable(detail.pictureRequest),
sizeChart: json(detail.sizeChart), sizeChart: json(detail.sizeChart),
packageSpecs: json(detail.packageSpecs), packageSpecs: json(detail.packageSpecs),
options: json(detail.options), options: json(detail.options),
media: json(detail.media), media: json(detail.media),
}; };
} }
private async replaceCustomVariants( private async replaceCustomVariants(
tx: Prisma.TransactionClient, tx: Prisma.TransactionClient,
originGoodId: bigint, originGoodId: bigint,
variants: CustomGoodVariantDto[], variants: CustomGoodVariantDto[],
): Promise<void> { ): Promise<void> {
await tx.originGoodVariant.deleteMany({ where: { originGoodId } }); await tx.originGoodVariant.deleteMany({ where: { originGoodId } });
for (const variant of variants) { for (const variant of variants) {
await tx.originGoodVariant.create({ await tx.originGoodVariant.create({
data: { data: {
originGoodId, originGoodId,
sdsVariantId: `custom-${randomUUID()}`, sdsVariantId: `custom-${randomUUID()}`,
sku: variant.sku, sku: variant.sku,
sizeId: variant.sizeId ?? null, sizeId: variant.sizeId ?? null,
sizeName: variant.sizeName ?? null, sizeName: variant.sizeName ?? null,
colorId: variant.colorId ?? null, colorId: variant.colorId ?? null,
colorName: variant.colorName ?? null, colorName: variant.colorName ?? null,
colorHex: variant.colorHex ?? null, colorHex: variant.colorHex ?? null,
imageUrl: variant.imageUrl ?? null, imageUrl: variant.imageUrl ?? null,
price: this.decimal(variant.price), price: this.decimal(variant.price),
originalPrice: this.decimal(variant.originalPrice), originalPrice: this.decimal(variant.originalPrice),
weightG: this.decimal(variant.weightG), weightG: this.decimal(variant.weightG),
boxLengthCm: this.decimal(variant.boxLengthCm), boxLengthCm: this.decimal(variant.boxLengthCm),
boxWidthCm: this.decimal(variant.boxWidthCm), boxWidthCm: this.decimal(variant.boxWidthCm),
boxHeightCm: this.decimal(variant.boxHeightCm), boxHeightCm: this.decimal(variant.boxHeightCm),
enabled: variant.enabled ?? true, enabled: variant.enabled ?? true,
sortOrder: variant.sortOrder ?? 0, sortOrder: variant.sortOrder ?? 0,
designData: designData:
variant.designData === null || variant.designData === undefined variant.designData === null || variant.designData === undefined
? Prisma.DbNull ? Prisma.DbNull
: (variant.designData as Prisma.InputJsonValue), : (variant.designData as Prisma.InputJsonValue),
}, },
}); });
} }
} }
} }