← back to Flock Fix Viewer
spec-osborne-dg.mjs
114 lines
// (b) Apply VERIFIED manufacturer specs to Osborne & Designers Guild active flock products.
// DG specs from designers_guild_catalog + designersguild.com; Osborne from osborne_catalog + osborneandlittle.com.
// Pattern-level EXACT match (colorway-agnostic; all colorways of a pattern share these). Reversible+ledgered+snapshot.
import fs from 'fs'; import os from 'os'; import path from 'path';
const SHOP='designer-laboratory-sandbox.myshopify.com';
const TOKEN=fs.readFileSync(new URL('./.token',import.meta.url),'utf8').trim();
const LEDGER=path.join(os.homedir(),'.claude/yolo-queue/executed-reversible/ledger.jsonl');
const SNAP=new URL('./spec-osborne-dg-snapshot.json',import.meta.url);
const DRY=!process.argv.includes('--apply');
const API=`https://${SHOP}/admin/api/2024-10`;
const H={'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'};
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const gql=async(q,v={})=>(await (await fetch(`${API}/graphql.json`,{method:'POST',headers:H,body:JSON.stringify({query:q,variables:v})})).json());
// verified per-pattern facts
const DG={
chennai:{w:'20.5 in',rep:'20.75 Inches',len:'32.8 ft',wi:20.5,lenft:32.8},
ardassa:{w:'20.5 in',rep:'20.5 Inches',len:'32.8 ft',wi:20.5,lenft:32.8},
mattiazzo:{w:'27 in',rep:'25.25 Inches',len:'32.8 ft',wi:27,lenft:32.8},
fioravanti:{w:'20.5 in',rep:'28.25 Inches',len:'32.8 ft',wi:20.5,lenft:32.8},
};
const DG_UNIFORM={match:'Straight',application:'Paste the Wall',backing:'Non-Woven',removal:'Strippable',print:'Flock',material:'Flocked Velvet',fire:'ASTM E84 Class A'};
const OSB={w:'20.5 in',len:'10m / 11 yd (single roll)',wi:20.5,lenft:32.8};
const OSB_UNIFORM={application:'Paste the Wall',backing:'Non-Woven',removal:'Strippable',print:'Flock',material:'Flocked Velvet'};
const r2=n=>Math.round(n*100)/100;
function trim1(html){ if(!html) return null;
const parts=html.split(/<\/p>/i).map(s=>s.replace(/<p[^>]*>/i,'').trim()).filter(s=>s.replace(/<[^>]+>/g,'').trim().length>3);
if(parts.length<=1) return null;
return '<p>'+parts[0]+'</p>';
}
function dgKey(t){t=(t||'').toLowerCase(); for(const k of Object.keys(DG)) if(t.includes(k)) return k; return null;}
async function fetchVendor(v){
let cur=null,all=[];
const Q=`query($c:String){ products(first:100, query:${JSON.stringify(`tag:'Flock Velvet' vendor:'${v}' status:active`)}, after:$c){ pageInfo{hasNextPage endCursor} edges{node{ id handle title descriptionHtml ow:metafield(namespace:"global",key:"width"){value} ol:metafield(namespace:"global",key:"length"){value} }}}}`;
do{const r=await gql(Q,{c:cur});const p=r?.data?.products;if(!p){console.error(JSON.stringify(r));break;}all=all.concat(p.edges);cur=p.pageInfo.hasNextPage?p.pageInfo.endCursor:null;}while(cur);
return all.map(e=>e.node).filter(n=>/flock/i.test(n.title));
}
// keys with a store definition requiring multi_line
const MULTILINE=new Set(['custom.material','custom.google_product_category']);
const typeFor=(ns,k)=>MULTILINE.has(ns+'.'+k)?'multi_line_text_field':'single_line_text_field';
function mfSet(id,pairs){
const mf=[];
for(const [gk,ck,val] of pairs){ if(val==null) continue;
mf.push({ownerId:id,namespace:'global',key:gk,type:typeFor('global',gk),value:val});
if(ck) mf.push({ownerId:id,namespace:'custom',key:ck,type:typeFor('custom',ck),value:val});
}
return mf;
}
(async()=>{
const snap=[]; let applied=0, skipped=[];
const dg=await fetchVendor('Designers Guild');
console.log('DG active flock:',dg.length);
for(const n of dg){
const k=dgKey(n.title);
if(!k){ skipped.push('DG no-pattern: '+n.title.slice(0,40)); continue; }
const s=DG[k];
const sr=r2((s.wi/12)*s.lenft), dr=r2(sr*2);
const pairs=[
['width',null,s.w],['length',null,s.len],
['repeat','pattern_repeat',s.rep],
['Match','match_type',DG_UNIFORM.match],
['application','application',DG_UNIFORM.application],
['Substrate','backing',DG_UNIFORM.backing],
['removability','removal',DG_UNIFORM.removal],
['print_type','print_type',DG_UNIFORM.print],
['material','material',DG_UNIFORM.material],
['fire_rating','fire_rating',DG_UNIFORM.fire],
['sqft_single_roll',null,sr+' Sq Ft'],['sqft_double_roll',null,dr+' Sq Ft'],
];
const newDesc=trim1(n.descriptionHtml);
if(DRY){ console.log(` DG[${k}] ${n.title.slice(0,38)} rep=${s.rep} SR=${sr} descTrim=${newDesc?'Y':'-'}`); continue; }
let mf=mfSet(n.id,pairs);
mf.push({ownerId:n.id,namespace:'custom',key:'sqft_single_roll',type:'single_line_text_field',value:sr+' Sq Ft'});
mf.push({ownerId:n.id,namespace:'custom',key:'sqft_double_roll',type:'single_line_text_field',value:dr+' Sq Ft'});
const rr=await gql('mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){userErrors{field message}}}',{mf});
const errs=rr?.data?.metafieldsSet?.userErrors; if(errs&&errs.length){console.log(' FAIL',n.handle,JSON.stringify(errs));continue;}
if(newDesc){ await gql('mutation($id:ID!,$b:String!){productUpdate(input:{id:$id,descriptionHtml:$b}){userErrors{message}}}',{id:n.id,b:newDesc}); }
snap.push({id:n.id,handle:n.handle,vendor:'Designers Guild',pattern:k,oldDesc:n.descriptionHtml||null,oldWidth:n.ow?.value||null,oldLength:n.ol?.value||null});
fs.appendFileSync(LEDGER,JSON.stringify({ts:new Date().toISOString(),agent:'vp-dw-commerce:spec-dg',ticket:'TK-11128',action:`DG verified specs (${k}: ${s.w},${s.rep},straight,non-woven,paste-the-wall,Class A,sqft) + 1-para desc on ${n.handle}`,blast_radius:1,undo_cmd:`delete added spec metafields on ${n.id}; restore desc from snapshot`,verify:'PDP spec table complete'})+'\n');
applied++; await sleep(220);
}
const osb=await fetchVendor('Osborne & Little');
console.log('Osborne active flock:',osb.length);
for(const n of osb){
if(!/regency/i.test(n.title)){ skipped.push('OSB non-regency: '+n.title.slice(0,40)); continue; }
const sr=r2((OSB.wi/12)*OSB.lenft), dr=r2(sr*2);
const pairs=[
['width',null,OSB.w],['length',null,OSB.len],
['application','application',OSB_UNIFORM.application],
['Substrate','backing',OSB_UNIFORM.backing],
['removability','removal',OSB_UNIFORM.removal],
['print_type','print_type',OSB_UNIFORM.print],
['material','material',OSB_UNIFORM.material],
['sqft_single_roll',null,sr+' Sq Ft'],['sqft_double_roll',null,dr+' Sq Ft'],
];
const newDesc=trim1(n.descriptionHtml);
if(DRY){ console.log(` OSB ${n.title.slice(0,38)} SR=${sr} descTrim=${newDesc?'Y':'-'}`); continue; }
let mf=mfSet(n.id,pairs);
mf.push({ownerId:n.id,namespace:'custom',key:'sqft_single_roll',type:'single_line_text_field',value:sr+' Sq Ft'});
mf.push({ownerId:n.id,namespace:'custom',key:'sqft_double_roll',type:'single_line_text_field',value:dr+' Sq Ft'});
const rr=await gql('mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){userErrors{field message}}}',{mf});
const errs=rr?.data?.metafieldsSet?.userErrors; if(errs&&errs.length){console.log(' FAIL',n.handle,JSON.stringify(errs));continue;}
if(newDesc){ await gql('mutation($id:ID!,$b:String!){productUpdate(input:{id:$id,descriptionHtml:$b}){userErrors{message}}}',{id:n.id,b:newDesc}); }
snap.push({id:n.id,handle:n.handle,vendor:'Osborne & Little',pattern:'regency stripe',oldDesc:n.descriptionHtml||null,oldWidth:n.ow?.value||null,oldLength:n.ol?.value||null});
fs.appendFileSync(LEDGER,JSON.stringify({ts:new Date().toISOString(),agent:'vp-dw-commerce:spec-osborne',ticket:'TK-11128',action:`Osborne Regency verified specs (20.5in,10m single,non-woven flock,paste-the-wall,sqft; repeat/match omitted-unverified) + 1-para desc on ${n.handle}`,blast_radius:1,undo_cmd:`delete added spec metafields on ${n.id}; restore desc`,verify:'PDP spec table (no repeat/match)'})+'\n');
applied++; await sleep(220);
}
if(!DRY) fs.writeFileSync(SNAP,JSON.stringify(snap,null,1));
console.log(`${DRY?'DRY':'DONE'} applied=${applied} skipped=${skipped.length}`);
if(skipped.length) console.log(' SKIP:',skipped.join(' | '));
})();