Files
inkreach-official-website/apps/api/src/sync/sds-product-detail.mapper.ts
T

262 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Prisma } from '@prisma/client';
import { SdsProductDetail, SdsProductVariant } from './sds-client.service';
type TableCell = { content?: unknown; remark?: unknown };
type Table = TableCell[][];
export interface NormalizedVariant {
sdsVariantId: string;
sku: string;
sizeId: string | null;
sizeName: string | null;
colorId: string | null;
colorName: string | null;
colorHex: string | null;
imageUrl: string | null;
price: Prisma.Decimal | null;
originalPrice: Prisma.Decimal | null;
weightG: Prisma.Decimal | null;
boxLengthCm: Prisma.Decimal | null;
boxWidthCm: Prisma.Decimal | null;
boxHeightCm: Prisma.Decimal | null;
enabled: boolean;
sortOrder: number;
designData: Prisma.InputJsonValue | null;
}
export interface NormalizedProductDetail {
productCode: string | null;
englishName: string | null;
blankDesignUrl: string | null;
detailsPageVideoUrl: string | null;
textureName: string | null;
productionCycleHours: number | null;
minWeightG: Prisma.Decimal | null;
reminder: string | null;
productionProcess: string | null;
materialDescription: string | null;
productPerformance: string | null;
applicableScenarios: string | null;
washingInstructions: string | null;
specialDescription: string | null;
designExplanation: string | null;
designArea: string | null;
pictureRequest: string | null;
sizeChart: Prisma.InputJsonValue | null;
packageSpecs: Prisma.InputJsonValue | null;
options: Prisma.InputJsonValue | null;
media: Prisma.InputJsonValue | null;
upstreamUpdatedAt: Date | null;
variants: NormalizedVariant[];
}
const text = (value: unknown): string | null => {
if (value === undefined || value === null) return null;
const normalized = String(value).trim();
return normalized.length > 0 ? normalized : null;
};
const decimal = (value: unknown): Prisma.Decimal | null => {
if (value === undefined || value === null || value === '') return null;
const n = Number(value);
return Number.isFinite(n) ? new Prisma.Decimal(n) : null;
};
const integer = (value: unknown): number | null => {
const n = Number(value);
return Number.isInteger(n) ? n : null;
};
function parseTable(raw: unknown): Table | null {
if (typeof raw !== 'string' || raw.trim() === '') return null;
try {
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed) || parsed.length < 2) return null;
const rows = parsed.filter(Array.isArray) as Table;
return rows.length >= 2 ? rows : null;
} catch {
return null;
}
}
const cell = (row: TableCell[], index: number): string =>
text(row[index]?.content)?.replace(/\t/g, '').trim() ?? '';
const measurementKey = (header: string, index: number): string => {
if (header.includes('衣长')) return 'bodyLength';
if (header.includes('胸围')) return 'chest';
if (header.includes('肩宽')) return 'shoulder';
if (header.includes('袖长')) return 'sleeveLength';
return `measurement${index}`;
};
export function parseSizeChart(raw: unknown): Prisma.InputJsonValue | null {
const table = parseTable(raw);
if (!table) return null;
const [header, ...body] = table;
const columns = header.slice(1).map((item, index) => {
const name = text(item.content)?.replace(/\s*\(cm\/in\)\s*/i, '') ?? `规格${index + 1}`;
return { key: measurementKey(name, index + 1), name };
});
const rows = body
.map((row, rowIndex) => {
const sizeName = cell(row, 0);
if (!sizeName) return null;
return {
sizeId: `size_${rowIndex}`,
sizeName,
measurements: columns.map((column, index) => {
const cm = cell(row, index + 1);
const cmNumber = Number(cm);
return {
key: column.key,
cm: cm || null,
in: Number.isFinite(cmNumber) ? (cmNumber / 2.54).toFixed(2) : null,
};
}),
};
})
.filter((row): row is NonNullable<typeof row> => row !== null);
return { columns, rows } as Prisma.InputJsonValue;
}
function dimensions(value: string): { length: string; width: string; height: string } | null {
const parts = value
.replace(/[×x]/gi, '*')
.split('*')
.map((part) => part.trim());
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(Number(part)))) return null;
return { length: parts[0], width: parts[1], height: parts[2] };
}
export function parsePackageSpecs(raw: unknown): Prisma.InputJsonValue | null {
const table = parseTable(raw);
if (!table) return null;
const rows = table.slice(1)
.map((row, rowIndex) => {
const sizeName = cell(row, 0);
if (!sizeName) return null;
return {
sizeId: `size_${rowIndex}`,
sizeName,
dimensionsCm: dimensions(cell(row, 1)),
dimensionsIn: dimensions(cell(row, 2)),
volumeCm3: cell(row, 3) || null,
volumeIn3: cell(row, 4) || null,
grossWeightG: cell(row, 5) || null,
grossWeightLb: cell(row, 6) || null,
};
})
.filter((row): row is NonNullable<typeof row> => row !== null);
return { rows } as Prisma.InputJsonValue;
}
function normalizeOptions(detail: SdsProductDetail): Prisma.InputJsonValue | null {
const attributers = detail.subproducts?.attributers;
if (!Array.isArray(attributers)) return null;
const sizeMap = new Map<string, { id: string; name: string; sortOrder: number; enabled: boolean }>();
const colorMap = new Map<string, { id: string; name: string; hex: string | null; sortOrder: number; enabled: boolean }>();
attributers.forEach((attribute, sizeIndex) => {
const sizeName = text(attribute.size);
const sizeId = text(attribute.sizeId) ?? `size_${sizeIndex}`;
if (sizeName) sizeMap.set(sizeId, { id: sizeId, name: sizeName, sortOrder: sizeIndex, enabled: true });
if (Array.isArray(attribute.colors)) {
attribute.colors.forEach((color, colorIndex) => {
const colorId = text(color.colorId) ?? `color_${colorIndex}`;
if (!colorMap.has(colorId)) {
colorMap.set(colorId, {
id: colorId,
name: text(color.chineseName) ?? text(color.color_name) ?? colorId,
hex: text(color.color),
sortOrder: integer(color.colorSort) ?? colorIndex,
enabled: true,
});
}
});
}
});
return { sizes: [...sizeMap.values()], colors: [...colorMap.values()] } as Prisma.InputJsonValue;
}
function normalizeMedia(detail: SdsProductDetail, variants: SdsProductVariant[]): Prisma.InputJsonValue | null {
const urls: string[] = [];
const add = (value: unknown) => {
const url = text(value);
if (url && !urls.includes(url)) urls.push(url);
};
add(detail.blankDesignUrl);
add(detail.psd_img_url);
add(detail.img_url);
for (const variant of variants) {
add(variant.psd_img_url);
add(variant.img_url);
add(variant.blankDesignUrl);
for (const image of variant.designPrototype?.detailImgUrls ?? []) add(image.imageUrl);
for (const image of variant.designPrototype?.prototypeResultGroups ?? []) add(image.resultImage);
}
if (urls.length === 0) return null;
return {
primaryImageUrl: urls[0],
images: urls.map((url, index) => ({ id: `image_${index}`, url, sortOrder: index })),
} as Prisma.InputJsonValue;
}
function normalizeVariant(variant: SdsProductVariant, index: number): NormalizedVariant | null {
const sdsVariantId = text(variant.id);
const sku = text(variant.sku);
if (!sdsVariantId || !sku) return null;
return {
sdsVariantId,
sku,
sizeId: text(variant.sizeId) ?? text(variant.sizeDto?.id),
sizeName: text(variant.size) ?? text(variant.sizeDto?.sizeName),
colorId: text(variant.colorId) ?? text(variant.color?.colorId),
colorName: text(variant.color?.chineseName) ?? text(variant.color_name) ?? text(variant.color?.color_name),
colorHex: text(variant.color?.color),
imageUrl: text(variant.psd_img_url) ?? text(variant.img_url) ?? text(variant.blankDesignUrl),
price: decimal(variant.currentPrice ?? variant.unit_price ?? variant.min_price),
originalPrice: decimal(variant.originalPrice),
weightG: decimal(variant.weight),
boxLengthCm: decimal(variant.box_length),
boxWidthCm: decimal(variant.box_width),
boxHeightCm: decimal(variant.box_height),
enabled: Number(variant.status ?? 1) === 1 && String(variant.delFlag ?? '0') === '0',
sortOrder: integer(variant.attribute_sort?.split('-')[0]) ?? integer(variant.size_sort) ?? index,
designData: variant.designPrototype ? (variant.designPrototype as Prisma.InputJsonValue) : null,
};
}
export function normalizeProductDetail(detail: SdsProductDetail): NormalizedProductDetail {
const productDetails = detail.product_details ?? {};
const sourceVariants = Array.isArray(detail.subproducts?.items) ? detail.subproducts!.items! : [];
const variants = sourceVariants
.map(normalizeVariant)
.filter((variant): variant is NormalizedVariant => variant !== null);
const updatedAt = Number(detail.updateTime);
return {
productCode: text(detail.sku),
englishName: text(detail.english_name),
blankDesignUrl: text(detail.blankDesignUrl),
detailsPageVideoUrl: text(detail.detailsPageVideoUrl),
textureName: text(detail.texture?.name),
productionCycleHours: integer(detail.productionCycle),
minWeightG: decimal(detail.minWeight),
reminder: text(productDetails.reminder),
productionProcess: text(productDetails.production_process),
materialDescription: text(productDetails.material_description),
productPerformance: text(productDetails.product_performance),
applicableScenarios: text(productDetails.applicable_scenarios),
washingInstructions: text(productDetails.washing_instructions),
specialDescription: text(productDetails.special_description),
designExplanation: text(productDetails.design_explanation),
designArea: text(productDetails.design_area),
pictureRequest: text(productDetails.picture_request),
sizeChart: parseSizeChart(productDetails.product_size),
packageSpecs: parsePackageSpecs(productDetails.packaging_specification),
options: normalizeOptions(detail),
media: normalizeMedia(detail, sourceVariants),
upstreamUpdatedAt: Number.isFinite(updatedAt) ? new Date(updatedAt) : null,
variants,
};
}