Files
justvitamin/scripts/sync-jv-product-truth.ts
T
Omair Saleh 056c47581f feat: editorial review dashboard + elite-grade pilot batch (5 SKUs)
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>
2026-06-02 18:50:09 +08:00

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}`)
}