#!/usr/bin/env bun import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' import { dirname, join } from 'path' const root = process.cwd() const migrationCsv = join(root, '..', 'JV Migration to shopify', 'products_import_lewis_v4.csv') const truthRoot = join(root, 'data', 'sources', 'jv-product-truth') const contentRoot = join(root, 'data', 'content', 'products') const generatedAt = new Date().toISOString() const skus = process.argv.slice(2) const targets = skus.length ? skus : ['JV-TURMERIC500', 'JV-DEFF1000', 'JV-VISISOFT', 'JV-D1000'] function parseCsv(text: string) { const rows: string[][] = [] let row: string[] = [] let cell = '' let quoted = false for (let i = 0; i < text.length; i++) { const char = text[i] const next = text[i + 1] if (char === '"') { if (quoted && next === '"') { cell += '"'; i++ } else quoted = !quoted } else if (char === ',' && !quoted) { row.push(cell); cell = '' } else if ((char === '\n' || char === '\r') && !quoted) { if (char === '\r' && next === '\n') i++ row.push(cell) if (row.some(value => value !== '')) rows.push(row) row = []; cell = '' } else cell += char } row.push(cell) if (row.some(value => value !== '')) rows.push(row) const headers = rows.shift() || [] return rows.map(values => Object.fromEntries(headers.map((header, index) => [header, values[index] || '']))) } function readJson(path: string, fallback: any = null) { if (!existsSync(path)) return fallback return JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, '')) } function writeJson(path: string, data: any) { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8') } function truthForSku(rows: any[], sku: string) { const variants = rows.filter(row => row['Variant SKU'] === sku || String(row['Variant SKU'] || '').startsWith(`${sku}-`)) const master = variants.find(row => row.Title || row['Body HTML']) || variants[0] if (!master) return null return { sku, syncedAt: generatedAt, sourcePath: migrationCsv, handle: master.Handle, title: master.Title, bodyHtml: master['Body HTML'], tags: master.Tags, imageSrc: master['Image Src'], status: master.Status, titleTag: master['Metafield: title_tag [string]'], descriptionTag: master['Metafield: description_tag [string]'], ingredients: master['Metafield: pdp.ingredients [rich_text_field]'], recommendedIntake: master['Metafield: pdp.recommended_intake [single_line_text_field]'], strength: master['Metafield: custom.strength [single_line_text_field]'] || master['Metafield: filter.strength [single_line_text_field]'], strapline: master['Metafield: pdp.strapline [single_line_text_field]'], format: master['Metafield: filter.format [single_line_text_field]'], legalCategory: master['Metafield: custom.legal_category [single_line_text_field]'], dimensions: master['Metafield: custom.dimensions [single_line_text_field]'], quickCode: master['Metafield: code.quickcode [single_line_text_field]'], uspList: master['Metafield: usp.list [list.single_line_text_field]'], warning: master['Metafield: pdp.warning [multi_line_text_field]'], allergens: master['Metafield: pdp.allergens [rich_text_field]'], guid: master['Metafield: code.guid [single_line_text_field]'], oldUrl: master['Metafield: temp.url [single_line_text_field]'], variants: variants.map(row => ({ sku: row['Variant SKU'], pdpTag: '', pdpColour: '', pdpPricePer: '', pdpStrapline: '', option: row['Option1 Value'], price: row['Variant Price'], compareAtPrice: row['Variant Compare At Price'], barcode: row['Variant Barcode'], inventoryQty: row['Variant Inventory Qty'], weight: row['Variant Weight'], weightUnit: row['Variant Weight Unit'], binLocation: row['Variant Metafield: shipping.bin_location [single_line_text_field]'], largeLetterFriendly: row['Variant Metafield: shipping.large_letter_friendly [boolean]'], packMultiplier: row['Variant Metafield: shipping.pack_multiplier [single_line_text_field]'] })) } } const rows = parseCsv(readFileSync(migrationCsv, 'utf8').replace(/^\uFEFF/, '')) for (const sku of targets) { const truth = truthForSku(rows, sku) if (!truth) { console.warn(`No JV truth row found for ${sku}`) continue } writeJson(join(truthRoot, `${sku}.json`), truth) const contentPath = join(contentRoot, `${sku}.json`) const existing = readJson(contentPath, { sku, metafields: {}, approval: {}, sources: {}, notes: [] }) const updated = { ...existing, sku, handle: existing.handle || truth.handle, name: truth.title || existing.name, variants: truth.variants.map((variant: any) => ({ ...(existing.variants || []).find((row: any) => row.sku === variant.sku), ...variant })), sources: { ...(existing.sources || {}), jvProductTruth: `data/sources/jv-product-truth/${sku}.json` }, notes: Array.from(new Set([ ...(existing.notes || []), 'Product truth synced from JV migration database export; internet sources must not override SKU, format, ingredients, intake, warnings, variants, price, barcode or stock truth.' ])), updatedAt: generatedAt } if (sku === 'JV-D1000') { updated.status = 'product_truth_synced_not_top3_effervescent' updated.metafields = { ...(updated.metafields || {}), filterStrength: truth.strength || updated.metafields?.filterStrength, pdpKeyFeatures: [ truth.strapline, truth.recommendedIntake, truth.format ? `Format: ${truth.format}` : '', 'This SKU is High Strength Vitamin D3 1000iu tablets, not the effervescent Vitamin D product.' ].filter(Boolean).join('\n') } } writeJson(contentPath, updated) console.log(`Synced JV truth for ${sku}: ${truth.title}`) }