← back to York Reprice 2026 08
build-onboard-payloads.mjs
91 lines
#!/usr/bin/env node
/**
* build-onboard-payloads.mjs — turn data/onboard-rows.json into data/onboard-payloads.json
* (same shape as york-new-phase1-payloads.json: pre-enrichment; enrich-onboard.py adds color+collections+tags).
*
* PRICING (critical):
* NATURALS (grasscloth/sisal/paperweave/cork/hemp/jute/raffia/abaca/arrowroot in technical_material)
* -> SINGLE-ROLL price from roll_price_single, min 2 / step 2, "Sold Per Single Roll".
* Else by UOM:
* BLT -> MAP (coalesce us_map, round(us_msrp*0.75,2)), roll, min 2 / step 2, "Sold Per Single Roll".
* YD -> MAP, sold-by-yard, min 1, "Sold By The Yard".
* EA -> MAP, sold each, min 1, "Sold Each" (murals/panels).
* BDR -> MAP, sold each, min 1, "Sold Each" (borders).
* NaN/absent price -> SKIP (never invent). Missing image / pattern_name -> SKIP.
*
* SKU allocation: DWJS-<n>, continuing after the max DWJS number already used by the 632
* (york-new-phase1-payloads-enriched.json). Sample variant = <SKU>-Sample @ $4.25, untracked.
*/
import fs from 'node:fs';
const rows=JSON.parse(fs.readFileSync('data/onboard-rows.json','utf8'));
const slug=s=>String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
const isNat=m=>/grasscloth|sisal|paperweave|paper weave|\bcork\b|\bhemp\b|\bjute\b|raffia|abaca|arrowroot/i.test(m||'');
const map=r=> (r.us_map>0?r.us_map : (r.us_msrp>0?Math.round(r.us_msrp*75)/100 : null));
// next SKU number after the 632
let maxN=91507;
try{ for(const p of JSON.parse(fs.readFileSync('data/york-new-phase1-payloads-enriched.json','utf8'))){ const m=/^DWJS-(\d+)$/.exec(p.variants[1].sku); if(m)maxN=Math.max(maxN,+m[1]); } }catch(e){}
let n=maxN+1;
const payloads=[]; const skipped=[]; const skipReason={};
for(const r of rows){
const pn=(r.pattern_name||'').trim(), cw=(r.colorway||'').trim();
// hard skips: no image, no pattern name
if(!r.image_url){ skipped.push([r.export_sku,'no-image']); skipReason['no-image']=(skipReason['no-image']||0)+1; continue; }
if(!pn){ skipped.push([r.export_sku,'no-pattern-name']); skipReason['no-pattern-name']=(skipReason['no-pattern-name']||0)+1; continue; }
// pricing branch
const nat=isNat(r.technical_material);
let price=null, optName=null, uom=null, min='1', step='1', pricedUom='Priced Per Single Roll';
if(nat){
price=r.roll_price_single; optName='Sold Per Single Roll'; uom='BLT'; min='2'; step='2'; pricedUom='Priced Per Single Roll';
} else {
const u=(r.uom||'').toUpperCase();
if(u==='YD'){ price=map(r); optName='Sold By The Yard'; uom='YD'; min='1'; step='1'; pricedUom='Priced Per Yard'; }
else if(u==='EA'||u==='BDR'){ price=map(r); optName='Sold Each'; uom=u; min='1'; step='1'; pricedUom='Priced Each'; }
else { // BLT + anything else roll-like
price=map(r); optName='Sold Per Single Roll'; uom='BLT'; min='2'; step='2'; pricedUom='Priced Per Single Roll';
}
}
const P=Number(price);
if(!(P>0) || !isFinite(P)){ skipped.push([r.export_sku,'no-price']); skipReason['no-price']=(skipReason['no-price']||0)+1; continue; }
const SKU=`DWJS-${n++}`;
const addCw = cw && !pn.toLowerCase().includes(cw.toLowerCase());
const title=`${pn}${addCw?' '+cw:''} | Jeffrey Stevens`;
const handle=slug(`${pn}-${cw}-wallpaper-${r.export_sku}`);
const mf={}; const put=(k,v)=>{ if(v!==null&&v!==undefined&&String(v).trim()!=='') mf[k]=String(v); };
put('custom.manufacturer_sku',r.export_sku); put('dwc.manufacturer_sku',r.export_sku);
put('custom.pattern_name',pn); put('custom.collection_name',r.book_name);
put('custom.brand_name',r.brand); put('custom.parent_brand',r.parent_brand);
put('custom.us_msrp',r.us_msrp); put('custom.wholesale_price',r.wholesale);
if(r.width_inches&&r.length_feet) put('global.width',`${r.width_inches}" x ${r.length_feet} ft`);
put('custom.width',r.width_inches); put('global.length',r.length_feet?`${r.length_feet} ft`:'');
put('global.repeat',r.pattern_repeat); put('custom.pattern_repeat',r.pattern_repeat);
put('global.MATCH',r.match_type); put('custom.match_type',r.match_type);
put('custom.backing',r.backing); put('specs.coverage',r.coverage);
put('specs.style',r.marketing_style); put('specs.pattern',r.marketing_pattern);
put('custom.color_family',r.color_family);
put('global.unit_of_measure',pricedUom);
put('global.v_prods_quantity_order_min',min); put('global.v_prods_quantity_order_units',step);
if(min==='2') put('global.packaged','Shipped in Doubles');
payloads.push({ mfr_sku:r.export_sku, title, handle, vendor:'Jeffrey Stevens', productType:'Wallcovering', status:'DRAFT',
descriptionHtml:r.dynamic_description||'',
options:[{name:'Size',values:['Sample',optName]}],
variants:[{sku:`${SKU}-Sample`,price:'4.25',option:'Sample',inventoryPolicy:'CONTINUE'},
{sku:SKU,price:P.toFixed(2),option:optName,inventoryPolicy:'CONTINUE'}],
image:r.image_url, metafields:mf, uom });
}
// dedupe any handle collisions (append mfr suffix already keeps them unique; guard anyway)
const seen=new Set();
for(const p of payloads){ let h=p.handle,i=2; while(seen.has(h)){h=`${p.handle}-${i++}`;} p.handle=h; seen.add(h); }
fs.writeFileSync('data/onboard-payloads.json',JSON.stringify(payloads,null,1));
const uomDist={};for(const p of payloads)uomDist[p.uom||'?']=(uomDist[p.uom||'?']||0)+1;
console.log(`built ${payloads.length} payloads | skipped ${skipped.length} ${JSON.stringify(skipReason)}`);
console.log(` UOM: ${JSON.stringify(uomDist)} | SKU ${payloads[0]?.variants[1].sku}..${payloads.at(-1)?.variants[1].sku}`);
console.log(` all DRAFT+priced: ${payloads.every(p=>p.status==='DRAFT'&&+p.variants[1].price>0)} | unique variant SKUs: ${new Set(payloads.flatMap(p=>p.variants.map(v=>v.sku))).size}/${payloads.length*2}`);
if(skipped.length) console.log(' skip samples:',skipped.slice(0,8).map(s=>s.join(':')).join(', '));
console.log('$0 (local)');