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 => 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 => 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(); const colorMap = new Map(); 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, }; }