← 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)');