feat(goods): add editable custom products
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsNumberString,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { CreateGoodDto } from './create-good.dto';
|
||||
|
||||
export class CustomGoodDetailDto {
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productCode?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
englishName?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
blankDesignUrl?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
detailsPageVideoUrl?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
textureName?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
productionCycleHours?: number | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true, example: '208.000' })
|
||||
@IsOptional()
|
||||
@IsNumberString()
|
||||
minWeightG?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productionProcess?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
materialDescription?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
reminder?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productPerformance?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
applicableScenarios?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
washingInstructions?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
specialDescription?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
designExplanation?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
designArea?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
pictureRequest?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true, type: Object })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
sizeChart?: Record<string, unknown> | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true, type: Object })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
packageSpecs?: Record<string, unknown> | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true, type: Object })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
options?: Record<string, unknown> | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true, type: Object })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
media?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export class CustomGoodVariantDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sku!: string;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sizeName?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sizeId?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
colorName?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
colorHex?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
colorId?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
imageUrl?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true, example: '28.00' })
|
||||
@IsOptional()
|
||||
@IsNumberString()
|
||||
price?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumberString()
|
||||
originalPrice?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumberString()
|
||||
weightG?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumberString()
|
||||
boxLengthCm?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumberString()
|
||||
boxWidthCm?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumberString()
|
||||
boxHeightCm?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true, type: Object })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
designData?: Record<string, unknown> | null;
|
||||
|
||||
@ApiProperty({ required: false, default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
enabled?: boolean;
|
||||
|
||||
@ApiProperty({ required: false, default: 0 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export class CreateCustomGoodDto extends OmitType(CreateGoodDto, [
|
||||
'originGoodId',
|
||||
] as const) {
|
||||
@ApiProperty({ required: false, nullable: true, example: '28.00' })
|
||||
@IsOptional()
|
||||
@IsNumberString()
|
||||
goodPrice?: string | null;
|
||||
|
||||
@ApiProperty({ required: false, type: CustomGoodDetailDto })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => CustomGoodDetailDto)
|
||||
detail?: CustomGoodDetailDto;
|
||||
|
||||
@ApiProperty({ required: false, type: [CustomGoodVariantDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CustomGoodVariantDto)
|
||||
variants?: CustomGoodVariantDto[];
|
||||
}
|
||||
|
||||
export class UpdateCustomGoodContentDto extends PartialType(
|
||||
OmitType(CreateCustomGoodDto, [
|
||||
'countryId',
|
||||
'categoryId',
|
||||
'tagIds',
|
||||
'positionId',
|
||||
'goodPriority',
|
||||
] as const),
|
||||
) {}
|
||||
@@ -9,6 +9,7 @@ export interface GoodRelations {
|
||||
originGood?: {
|
||||
id: bigint;
|
||||
sdsGoodId: string;
|
||||
source: 'SDS' | 'CUSTOM';
|
||||
goodName: string | null;
|
||||
goodImage: string | null;
|
||||
goodPrice: unknown;
|
||||
@@ -87,6 +88,8 @@ export class GoodDto {
|
||||
originGood?: {
|
||||
id: string;
|
||||
sdsGoodId: string;
|
||||
source: 'SDS' | 'CUSTOM';
|
||||
isCustom: boolean;
|
||||
goodName: string | null;
|
||||
goodImage: string | null;
|
||||
goodPrice: string | null;
|
||||
@@ -154,6 +157,8 @@ export class GoodDto {
|
||||
? {
|
||||
id: rel.originGood.id.toString(),
|
||||
sdsGoodId: rel.originGood.sdsGoodId,
|
||||
source: rel.originGood.source,
|
||||
isCustom: rel.originGood.source === 'CUSTOM',
|
||||
goodName: rel.originGood.goodName,
|
||||
goodImage: rel.originGood.goodImage,
|
||||
goodPrice:
|
||||
|
||||
@@ -22,6 +22,10 @@ import { UpdateGoodDto } from './dto/update-good.dto';
|
||||
import { QueryGoodDto } from './dto/query-good.dto';
|
||||
import { BatchCreateGoodDto } from './dto/batch-create-good.dto';
|
||||
import { BatchPriorityDto } from './dto/batch-priority.dto';
|
||||
import {
|
||||
CreateCustomGoodDto,
|
||||
UpdateCustomGoodContentDto,
|
||||
} from './dto/custom-good.dto';
|
||||
|
||||
@ApiTags('goods')
|
||||
@ApiBearerAuth()
|
||||
@@ -54,6 +58,21 @@ export class GoodsController {
|
||||
return this.service.batchCreate(dto);
|
||||
}
|
||||
|
||||
@Post('custom')
|
||||
@ApiOperation({ summary: 'Create a fully editable custom product' })
|
||||
createCustom(@Body() dto: CreateCustomGoodDto) {
|
||||
return this.service.createCustom(dto);
|
||||
}
|
||||
|
||||
@Patch(':id/custom-content')
|
||||
@ApiOperation({ summary: 'Update editable content for a custom product' })
|
||||
updateCustomContent(
|
||||
@Param('id', ParseIntPipe) id: string,
|
||||
@Body() dto: UpdateCustomGoodContentDto,
|
||||
) {
|
||||
return this.service.updateCustomContent(BigInt(id), dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get one good with relations' })
|
||||
findOne(@Param('id', ParseIntPipe) id: string) {
|
||||
|
||||
@@ -119,6 +119,51 @@ describe('GoodsService', () => {
|
||||
expect(fetched.goodName).toBe(`Goods Test ${stamp} basic`);
|
||||
});
|
||||
|
||||
it('creates, edits, and removes a fully editable custom good', async () => {
|
||||
const created = await service.createCustom({
|
||||
goodName: `Goods Test ${stamp} custom`,
|
||||
goodImage: 'https://example.com/custom.png',
|
||||
goodPrice: '29.90',
|
||||
countryId: Number(countryId),
|
||||
categoryId: Number(categoryId),
|
||||
tagIds: [Number(tagId)],
|
||||
detail: {
|
||||
productCode: `CUSTOM-${stamp}`,
|
||||
materialDescription: 'Cotton',
|
||||
sizeChart: { columns: [], rows: [] },
|
||||
packageSpecs: { rows: [] },
|
||||
},
|
||||
variants: [
|
||||
{ sku: `CUSTOM-SKU-${stamp}`, sizeName: 'S', price: '29.90' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(created.originGood?.source).toBe('CUSTOM');
|
||||
expect(created.originGood?.isCustom).toBe(true);
|
||||
expect(created.originGood?.goodPrice).toBe('29.9');
|
||||
expect(created.variants).toHaveLength(1);
|
||||
|
||||
const updated = await service.updateCustomContent(BigInt(created.id), {
|
||||
goodName: `Goods Test ${stamp} custom edited`,
|
||||
goodPrice: '39.90',
|
||||
detail: { materialDescription: 'Organic cotton' },
|
||||
variants: [
|
||||
{ sku: `CUSTOM-SKU-${stamp}-M`, sizeName: 'M', price: '39.90' },
|
||||
],
|
||||
});
|
||||
expect(updated.goodName).toContain('custom edited');
|
||||
expect(updated.originGood?.goodPrice).toBe('39.9');
|
||||
expect(updated.originDetail?.materialDescription).toBe('Organic cotton');
|
||||
expect(updated.originDetail?.productCode).toBe(`CUSTOM-${stamp}`);
|
||||
expect(updated.variants[0]?.sizeName).toBe('M');
|
||||
|
||||
const customOriginId = BigInt(updated.originGoodId);
|
||||
await service.remove(BigInt(updated.id));
|
||||
await expect(
|
||||
prisma.originGood.findUnique({ where: { id: customOriginId } }),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('filters by countryId, tagId, positionId and keyword', async () => {
|
||||
const result = await service.findAll({
|
||||
page: 1,
|
||||
|
||||
@@ -12,6 +12,13 @@ import { BatchCreateGoodDto } from './dto/batch-create-good.dto';
|
||||
import { BatchPriorityDto } from './dto/batch-priority.dto';
|
||||
import { GoodDetailDto, GoodDto, PaginatedGoods } from './dto/good.dto';
|
||||
import { SyncService } from '../sync/sync.service';
|
||||
import { randomUUID } from 'crypto';
|
||||
import {
|
||||
CreateCustomGoodDto,
|
||||
CustomGoodDetailDto,
|
||||
CustomGoodVariantDto,
|
||||
UpdateCustomGoodContentDto,
|
||||
} from './dto/custom-good.dto';
|
||||
|
||||
const GOOD_INCLUDE = {
|
||||
country: true,
|
||||
@@ -126,12 +133,117 @@ export class GoodsService {
|
||||
goodTags: result.goodTags,
|
||||
});
|
||||
});
|
||||
if (result.originGood?.sdsGoodId && !result.originGood.hasDetail) {
|
||||
if (
|
||||
result.originGood?.source === 'SDS' &&
|
||||
result.originGood.sdsGoodId &&
|
||||
!result.originGood.hasDetail
|
||||
) {
|
||||
this.syncService.queueProductDetailSync(result.originGood.sdsGoodId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async createCustom(dto: CreateCustomGoodDto): Promise<GoodDetailDto> {
|
||||
await this.ensureCountry(dto.countryId);
|
||||
await this.ensureCategory(dto.categoryId);
|
||||
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({
|
||||
data: {
|
||||
source: 'CUSTOM',
|
||||
sdsGoodId: `custom-${randomUUID()}`,
|
||||
goodName: dto.goodName,
|
||||
goodImage: dto.goodImage ?? null,
|
||||
goodPrice: this.decimal(dto.goodPrice),
|
||||
detail: {
|
||||
create: this.customDetailData(
|
||||
dto.detail ?? {},
|
||||
) as Prisma.OriginGoodDetailUncheckedCreateWithoutOriginGoodInput,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (dto.variants?.length) {
|
||||
await this.replaceCustomVariants(tx, originGood.id, dto.variants);
|
||||
}
|
||||
const good = await tx.good.create({
|
||||
data: {
|
||||
originGoodId: originGood.id,
|
||||
countryId: BigInt(dto.countryId),
|
||||
categoryId: BigInt(dto.categoryId),
|
||||
positionId:
|
||||
dto.positionId === undefined ? null : BigInt(dto.positionId),
|
||||
goodName: dto.goodName,
|
||||
goodImage: dto.goodImage ?? null,
|
||||
goodPriority: dto.goodPriority ?? 0,
|
||||
},
|
||||
});
|
||||
if (dto.tagIds?.length) {
|
||||
await tx.goodTag.createMany({
|
||||
data: dto.tagIds.map((tagId) => ({
|
||||
goodId: good.id,
|
||||
tagId: BigInt(tagId),
|
||||
})),
|
||||
});
|
||||
}
|
||||
return good.id;
|
||||
});
|
||||
return this.findOne(goodId);
|
||||
}
|
||||
|
||||
async updateCustomContent(
|
||||
id: bigint,
|
||||
dto: UpdateCustomGoodContentDto,
|
||||
): Promise<GoodDetailDto> {
|
||||
const existing = await this.prisma.good.findUnique({
|
||||
where: { id },
|
||||
include: { originGood: true },
|
||||
});
|
||||
if (!existing) throw new NotFoundException(`Good ${id} not found`);
|
||||
if (existing.originGood.source !== 'CUSTOM') {
|
||||
throw new BadRequestException('SDS 映射商品的上游信息不可修改');
|
||||
}
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.originGood.update({
|
||||
where: { id: existing.originGoodId },
|
||||
data: {
|
||||
goodName: dto.goodName,
|
||||
goodImage: dto.goodImage,
|
||||
goodPrice:
|
||||
dto.goodPrice === undefined ? undefined : this.decimal(dto.goodPrice),
|
||||
},
|
||||
});
|
||||
if (dto.detail !== undefined) {
|
||||
await tx.originGoodDetail.upsert({
|
||||
where: { originGoodId: existing.originGoodId },
|
||||
create: {
|
||||
originGoodId: existing.originGoodId,
|
||||
...(this.customDetailData(
|
||||
dto.detail,
|
||||
) as Prisma.OriginGoodDetailUncheckedCreateWithoutOriginGoodInput),
|
||||
},
|
||||
update: this.customDetailData(dto.detail, true),
|
||||
});
|
||||
}
|
||||
if (dto.variants !== undefined) {
|
||||
await this.replaceCustomVariants(
|
||||
tx,
|
||||
existing.originGoodId,
|
||||
dto.variants,
|
||||
);
|
||||
}
|
||||
const goodData: Prisma.GoodUpdateInput = {};
|
||||
if (dto.goodName !== undefined) goodData.goodName = dto.goodName;
|
||||
if (dto.goodImage !== undefined) goodData.goodImage = dto.goodImage;
|
||||
if (Object.keys(goodData).length) {
|
||||
await tx.good.update({ where: { id }, data: goodData });
|
||||
}
|
||||
});
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateGoodDto): Promise<GoodDto> {
|
||||
await this.findOne(id);
|
||||
const data: Prisma.GoodUpdateInput = {};
|
||||
@@ -188,15 +300,33 @@ export class GoodsService {
|
||||
goodTags: updated.goodTags,
|
||||
});
|
||||
});
|
||||
if (result.originGood?.sdsGoodId && !result.originGood.hasDetail) {
|
||||
if (
|
||||
result.originGood?.source === 'SDS' &&
|
||||
result.originGood.sdsGoodId &&
|
||||
!result.originGood.hasDetail
|
||||
) {
|
||||
this.syncService.queueProductDetailSync(result.originGood.sdsGoodId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async remove(id: bigint): Promise<{ id: string }> {
|
||||
await this.findOne(id);
|
||||
await this.prisma.good.delete({ where: { id } });
|
||||
const good = await this.prisma.good.findUnique({
|
||||
where: { id },
|
||||
include: { originGood: true },
|
||||
});
|
||||
if (!good) throw new NotFoundException(`Good ${id} not found`);
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.good.delete({ where: { id } });
|
||||
if (good.originGood.source === 'CUSTOM') {
|
||||
const remaining = await tx.good.count({
|
||||
where: { originGoodId: good.originGoodId },
|
||||
});
|
||||
if (remaining === 0) {
|
||||
await tx.originGood.delete({ where: { id: good.originGoodId } });
|
||||
}
|
||||
}
|
||||
});
|
||||
return { id: id.toString() };
|
||||
}
|
||||
|
||||
@@ -275,7 +405,11 @@ export class GoodsService {
|
||||
});
|
||||
for (const goodId of new Set(
|
||||
result
|
||||
.filter((item) => !item.originGood?.hasDetail)
|
||||
.filter(
|
||||
(item) =>
|
||||
item.originGood?.source === 'SDS' &&
|
||||
!item.originGood.hasDetail,
|
||||
)
|
||||
.map((item) => item.originGood?.sdsGoodId)
|
||||
.filter((id): id is string => Boolean(id)),
|
||||
)) {
|
||||
@@ -341,4 +475,93 @@ export class GoodsService {
|
||||
if (!p) throw new BadRequestException(`Position ${dto.positionId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensurePosition(id: number) {
|
||||
const position = await this.prisma.position.findUnique({
|
||||
where: { id: BigInt(id) },
|
||||
});
|
||||
if (!position) throw new BadRequestException(`Position ${id} not found`);
|
||||
}
|
||||
|
||||
private decimal(value: string | null | undefined): Prisma.Decimal | null {
|
||||
return value === undefined || value === null || value === ''
|
||||
? null
|
||||
: new Prisma.Decimal(value);
|
||||
}
|
||||
|
||||
private customDetailData(
|
||||
detail: CustomGoodDetailDto,
|
||||
preserveMissing = false,
|
||||
): Prisma.OriginGoodDetailUncheckedUpdateInput {
|
||||
const nullable = <T>(value: T | null | undefined): T | null | undefined =>
|
||||
preserveMissing && value === undefined ? undefined : value ?? null;
|
||||
const decimal = (value: string | null | undefined) =>
|
||||
preserveMissing && value === undefined ? undefined : this.decimal(value);
|
||||
const json = (
|
||||
value: Record<string, unknown> | null | undefined,
|
||||
): Prisma.InputJsonValue | Prisma.NullTypes.DbNull | undefined =>
|
||||
preserveMissing && value === undefined
|
||||
? undefined
|
||||
: value === null || value === undefined
|
||||
? Prisma.DbNull
|
||||
: (value as Prisma.InputJsonValue);
|
||||
return {
|
||||
productCode: nullable(detail.productCode),
|
||||
englishName: nullable(detail.englishName),
|
||||
blankDesignUrl: nullable(detail.blankDesignUrl),
|
||||
detailsPageVideoUrl: nullable(detail.detailsPageVideoUrl),
|
||||
textureName: nullable(detail.textureName),
|
||||
productionCycleHours: nullable(detail.productionCycleHours),
|
||||
minWeightG: decimal(detail.minWeightG),
|
||||
reminder: nullable(detail.reminder),
|
||||
productionProcess: nullable(detail.productionProcess),
|
||||
materialDescription: nullable(detail.materialDescription),
|
||||
productPerformance: nullable(detail.productPerformance),
|
||||
applicableScenarios: nullable(detail.applicableScenarios),
|
||||
washingInstructions: nullable(detail.washingInstructions),
|
||||
specialDescription: nullable(detail.specialDescription),
|
||||
designExplanation: nullable(detail.designExplanation),
|
||||
designArea: nullable(detail.designArea),
|
||||
pictureRequest: nullable(detail.pictureRequest),
|
||||
sizeChart: json(detail.sizeChart),
|
||||
packageSpecs: json(detail.packageSpecs),
|
||||
options: json(detail.options),
|
||||
media: json(detail.media),
|
||||
};
|
||||
}
|
||||
|
||||
private async replaceCustomVariants(
|
||||
tx: Prisma.TransactionClient,
|
||||
originGoodId: bigint,
|
||||
variants: CustomGoodVariantDto[],
|
||||
): Promise<void> {
|
||||
await tx.originGoodVariant.deleteMany({ where: { originGoodId } });
|
||||
for (const variant of variants) {
|
||||
await tx.originGoodVariant.create({
|
||||
data: {
|
||||
originGoodId,
|
||||
sdsVariantId: `custom-${randomUUID()}`,
|
||||
sku: variant.sku,
|
||||
sizeId: variant.sizeId ?? null,
|
||||
sizeName: variant.sizeName ?? null,
|
||||
colorId: variant.colorId ?? null,
|
||||
colorName: variant.colorName ?? null,
|
||||
colorHex: variant.colorHex ?? null,
|
||||
imageUrl: variant.imageUrl ?? null,
|
||||
price: this.decimal(variant.price),
|
||||
originalPrice: this.decimal(variant.originalPrice),
|
||||
weightG: this.decimal(variant.weightG),
|
||||
boxLengthCm: this.decimal(variant.boxLengthCm),
|
||||
boxWidthCm: this.decimal(variant.boxWidthCm),
|
||||
boxHeightCm: this.decimal(variant.boxHeightCm),
|
||||
enabled: variant.enabled ?? true,
|
||||
sortOrder: variant.sortOrder ?? 0,
|
||||
designData:
|
||||
variant.designData === null || variant.designData === undefined
|
||||
? Prisma.DbNull
|
||||
: (variant.designData as Prisma.InputJsonValue),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user