056c47581f
Ships the second dashboard surface — a Pattern Library + Preview Theatre — that presents the 4-section PDP pilot batch back to Umar, compliance, and the board in an editorial format. Adds the full data layer that drives it: 5 source-backed per-SKU drafts at QA 100/100, 15 competitor PDP semantic extracts, PubMed evidence packs, EFSA claims library extension, JV brand voice guide, hand-curated product FAQs, and the Matrixify-ready CSV exports for Lewis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
5.8 KiB
TypeScript
138 lines
5.8 KiB
TypeScript
#!/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}`)
|
|
}
|