← back to Hollywood Import
TK-11337: promote Dana Point Split Rock residual to per-yard (XZW-511904), reversible executor
8fa9801003ba8c287d32829c23c8f6019a91b07b · 2026-09-10 13:16:18 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKGgGfzp7nM28uepd8gfFF
Files touched
A tk11337-danapoint-promote.mjsA tk11337-danapoint-restore-map.json
Diff
commit 8fa9801003ba8c287d32829c23c8f6019a91b07b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 10 13:16:18 2026 -0700
TK-11337: promote Dana Point Split Rock residual to per-yard (XZW-511904), reversible executor
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKGgGfzp7nM28uepd8gfFF
---
tk11337-danapoint-promote.mjs | 122 +++++++++++++++++++++++++++++++++++++
tk11337-danapoint-restore-map.json | 34 +++++++++++
2 files changed, 156 insertions(+)
diff --git a/tk11337-danapoint-promote.mjs b/tk11337-danapoint-promote.mjs
new file mode 100644
index 0000000..83a1546
--- /dev/null
+++ b/tk11337-danapoint-promote.mjs
@@ -0,0 +1,122 @@
+// TK-11337 — promote the 1 genuine sample-only residual (Dana Point – Split Rock, pid 7694269251635)
+// to a sellable per-yard product, matching the proven 919/812 Hollywood standard.
+// • add "Sold Per Yard" variant @ $56.83 (wholesale 39.25 × 1.448, confirmed), sku XZW-511904-yard,
+// inventoryPolicy CONTINUE, tracked, on_hand 2026 @ Location/5795643504
+// • reorder → yard pos1, sample pos2
+// • relabel sample sku DWHD-500256-Sample → XZW-511904-sample (fresh opaque XZW identity; no vendor leak)
+// • metafields: global.unit_of_measure='Sold Per Yard', v_prods_quantity_order_min='5',
+// v_prods_quantity_order_units='1', global.dw_sku / dwc.dw_sku='XZW-511904', global.length='30 yards'
+// • register XZW-511904 in dw_sku_registry; update momentum_colorways.dw_sku (Mac2 staging)
+// • NO "Type II" (product is 10.72oz, 60% glass fiber / 40% bio-based — Type II would be a false spec)
+// Capture-first restore-map; --apply gated; --rollback reverses via the captured map.
+import fs from 'node:fs'; import { execFileSync } from 'node:child_process';
+const env = fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8');
+const STORE=(env.match(/^SHOPIFY_STORE=(.+)$/m)||[])[1].trim();
+const TOKEN=((env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)||env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m))||[])[1].trim(); // FULL first (needs write_inventory)
+const API=`https://${STORE}/admin/api/2024-10`;
+const GQL=`${API}/graphql.json`;
+const H={'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'};
+const PID='7694269251635', GID=`gid://shopify/Product/${PID}`;
+const DW='XZW-511904', YARD_SKU=`${DW}-yard`, SAMP_SKU=`${DW}-sample`, PRICE='56.83';
+const LOC='gid://shopify/Location/5795643504', QTY=2026;
+const MFR='WSW-AL-03', HANDLE='alys-texture-ws-split-rock-wallcovering';
+const MAP=new URL('tk11337-danapoint-restore-map.json',import.meta.url).pathname;
+const APPLY=process.argv.includes('--apply'), ROLLBACK=process.argv.includes('--rollback');
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+async function gql(q,v){const r=await fetch(GQL,{method:'POST',headers:H,body:JSON.stringify({query:q,variables:v})});const j=await r.json();if(j.errors)throw new Error(JSON.stringify(j.errors));return j.data;}
+async function rest(path,method,body){const r=await fetch(`${API}/${path}`,{method,headers:H,body:body?JSON.stringify(body):undefined});const t=await r.text();let j;try{j=JSON.parse(t)}catch{j={raw:t}}if(r.status>=300)throw new Error(`${method} ${path} → ${r.status} ${t.slice(0,200)}`);return j;}
+const psql=sql=>execFileSync('psql',['-d','dw_unified','-tA','-c',sql],{env:{...process.env,PGHOST:'/tmp'},encoding:'utf8'}).trim();
+const LOOKUP=`query($id:ID!){node(id:$id){... on Product{id status options{name} variants(first:20){nodes{id title price sku inventoryPolicy inventoryItem{id}}}}}}`;
+const BULK=`mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){productVariantsBulkCreate(productId:$pid,variants:$variants){productVariants{id price sku title inventoryItem{id}} userErrors{field message}}}`;
+const M_TRACK=`mutation($id:ID!){inventoryItemUpdate(id:$id,input:{tracked:true}){userErrors{message}}}`;
+const M_ACT=`mutation($iid:ID!,$loc:ID!){inventoryActivate(inventoryItemId:$iid,locationId:$loc){userErrors{message}}}`;
+const M_QTY=`mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{message}}}`;
+
+async function capture(){
+ if(fs.existsSync(MAP)){const s=JSON.parse(fs.readFileSync(MAP,'utf8'));console.log(' (preserving existing restore-map baseline ts='+s.ts+')');return s;}
+ const p=(await gql(LOOKUP,{id:GID})).node;
+ const mf=(await rest(`products/${PID}/metafields.json?limit=250`,'GET')).metafields||[];
+ const snap={ts:new Date().toISOString(),pid:PID,handle:HANDLE,status:p.status,options:p.options,
+ variants:p.variants.nodes.map(v=>({id:v.id,title:v.title,price:v.price,sku:v.sku,policy:v.inventoryPolicy})),
+ mint:DW, existing_metafield_keys:mf.filter(m=>['global','dwc'].includes(m.namespace)).map(m=>`${m.namespace}.${m.key}`)};
+ fs.writeFileSync(MAP,JSON.stringify(snap,null,2));
+ return snap;
+}
+async function upsertMF(namespace,key,type,value){
+ await rest(`products/${PID}/metafields.json`,'POST',{metafield:{namespace,key,type,value}}).catch(async e=>{
+ // if exists, find + update
+ const mf=(await rest(`products/${PID}/metafields.json?limit=250`,'GET')).metafields||[];
+ const ex=mf.find(m=>m.namespace===namespace&&m.key===key);
+ if(ex) await rest(`metafields/${ex.id}.json`,'PUT',{metafield:{id:ex.id,type,value}}); else throw e;
+ });
+}
+
+(async()=>{
+ if(ROLLBACK){
+ const snap=JSON.parse(fs.readFileSync(MAP,'utf8'));
+ console.log('ROLLBACK from',snap.ts);
+ const p=(await gql(LOOKUP,{id:GID})).node;
+ // delete the yard variant
+ const yard=p.variants.nodes.find(v=>v.sku===YARD_SKU);
+ if(yard){await rest(`products/${PID}/variants/${yard.id.split('/').pop()}.json`,'DELETE');console.log(' deleted yard variant');}
+ // restore sample sku
+ const samp=p.variants.nodes.find(v=>/-sample$/i.test(v.sku));
+ const oldSamp=snap.variants[0];
+ if(samp&&oldSamp&&samp.sku!==oldSamp.sku){await rest(`products/${PID}/variants/${samp.id.split('/').pop()}.json`,'PUT',{variant:{id:samp.id.split('/').pop(),sku:oldSamp.sku}});console.log(' restored sample sku →',oldSamp.sku);}
+ // delete metafields we created (those NOT in existing_metafield_keys)
+ const created=[['global','unit_of_measure'],['global','v_prods_quantity_order_min'],['global','v_prods_quantity_order_units'],['global','dw_sku'],['dwc','dw_sku'],['global','length']].filter(([ns,k])=>!snap.existing_metafield_keys.includes(`${ns}.${k}`));
+ const mf=(await rest(`products/${PID}/metafields.json?limit=250`,'GET')).metafields||[];
+ for(const [ns,k] of created){const ex=mf.find(m=>m.namespace===ns&&m.key===k);if(ex){await rest(`metafields/${ex.id}.json`,'DELETE');console.log(' deleted mf',ns+'.'+k);}}
+ psql(`DELETE FROM dw_sku_registry WHERE dw_sku='${DW}';`);
+ psql(`UPDATE momentum_colorways SET dw_sku=NULL WHERE (upper(pattern_sku)='${MFR}' OR upper(alt_sku)='${MFR}') AND dw_sku='${DW}';`);
+ console.log(' registry + staging reverted. ROLLBACK done.');
+ return;
+ }
+ const snap=await capture();
+ console.log('CAPTURED restore-map →',MAP);
+ console.log('BEFORE:',JSON.stringify(snap.variants),'options:',JSON.stringify(snap.options));
+ console.log(`PLAN: +"Sold Per Yard" $${PRICE} sku ${YARD_SKU} (CONTINUE, qty ${QTY}); sample→${SAMP_SKU}; yard pos1; mint ${DW}; 5yd min; NO Type II.`);
+ if(!APPLY){console.log('\nDRY-RUN. Re-run with --apply to execute.');return;}
+
+ const p=snap; const optName=(p.options[0]&&p.options[0].name)||'Title';
+ // 1) ensure yard variant (idempotent resume)
+ let live=(await gql(LOOKUP,{id:GID})).node.variants.nodes;
+ let yardV=live.find(v=>v.sku===YARD_SKU);
+ if(yardV){console.log(' = yard variant already exists',yardV.id,'(resuming)');}
+ else{
+ if(!(live.length===1&&parseFloat(live[0].price)<=4.30)){console.log('GUARD: not sample-only and no yard variant — aborting, no change.');return;}
+ const r=await gql(BULK,{pid:GID,variants:[{price:PRICE,optionValues:[{optionName:optName,name:'Sold Per Yard'}],inventoryPolicy:'CONTINUE',inventoryItem:{sku:YARD_SKU,tracked:true}}]});
+ const ue=r.productVariantsBulkCreate.userErrors||[];if(ue.length)throw new Error('bulkCreate: '+JSON.stringify(ue));
+ yardV=r.productVariantsBulkCreate.productVariants.find(v=>v.sku===YARD_SKU);
+ console.log(' ✓ yard variant',yardV.id,yardV.sku,'$'+yardV.price);
+ }
+ // self-heal sample if consumed
+ let after=(await gql(LOOKUP,{id:GID})).node.variants.nodes;
+ if(!after.some(v=>/sample/i.test(v.title)||/-sample$/i.test(v.sku||''))){
+ await gql(BULK,{pid:GID,variants:[{price:'4.25',optionValues:[{optionName:optName,name:'Sample'}],inventoryPolicy:'DENY',inventoryItem:{sku:SAMP_SKU,tracked:false}}]});
+ console.log(' ✓ sample re-healed');
+ after=(await gql(LOOKUP,{id:GID})).node.variants.nodes;
+ }
+ // 2) inventory 2026 on yard
+ if(yardV.inventoryItem?.id){const iid=yardV.inventoryItem.id;await gql(M_TRACK,{id:iid});await gql(M_ACT,{iid,loc:LOC});await gql(M_QTY,{input:{name:'on_hand',reason:'correction',ignoreCompareQuantity:true,quantities:[{inventoryItemId:iid,locationId:LOC,quantity:QTY}]}});console.log(' ✓ inventory 2026');}
+ // 3) relabel sample sku (only if it still carries the old code, and wasn't just re-healed to SAMP_SKU)
+ const samp=after.find(v=>/sample/i.test(v.title)||/-sample$/i.test(v.sku||''));
+ if(samp&&samp.sku!==SAMP_SKU){const nid=samp.id.split('/').pop();await rest(`products/${PID}/variants/${nid}.json`,'PUT',{variant:{id:nid,sku:SAMP_SKU}});console.log(' ✓ sample sku →',SAMP_SKU);}
+ // 4) reorder: yard pos1, sample pos2
+ const yId=yardV.id.split('/').pop(); const sId=(after.find(v=>/sample/i.test(v.title)||/-sample$/i.test(v.sku||'')).id).split('/').pop();
+ await rest(`products/${PID}.json`,'PUT',{product:{id:PID,variants:[{id:yId,position:1},{id:sId,position:2}]}});
+ console.log(' ✓ reordered (yard pos1)');
+ // 5) metafields
+ await upsertMF('global','unit_of_measure','single_line_text_field','Sold Per Yard');
+ await upsertMF('global','v_prods_quantity_order_min','single_line_text_field','5');
+ await upsertMF('global','v_prods_quantity_order_units','single_line_text_field','1');
+ await upsertMF('global','length','single_line_text_field','30 yards');
+ await upsertMF('global','dw_sku','single_line_text_field',DW);
+ await upsertMF('dwc','dw_sku','single_line_text_field',DW);
+ console.log(' ✓ metafields set (unit/min/units/length/dw_sku ×2) — NO Type II');
+ // 6) register + staging
+ psql(`INSERT INTO dw_sku_registry (dw_sku,vendor_prefix,vendor_name,mfr_sku,shopify_product_id,shopify_handle,status,min_order_qty,order_increment,created_at,updated_at) VALUES ('${DW}','XZW','Hollywood Wallcoverings','${MFR}',${PID},'${HANDLE}','ACTIVE',5,1,now(),now()) ON CONFLICT DO NOTHING;`);
+ psql(`UPDATE momentum_colorways SET dw_sku='${DW}',updated_at=now() WHERE (upper(pattern_sku)='${MFR}' OR upper(alt_sku)='${MFR}');`);
+ console.log(' ✓ registered XZW-511904 + staging dw_sku set');
+ console.log('\nDONE.');
+})().catch(e=>{console.error('FATAL',e.message);process.exit(1);});
diff --git a/tk11337-danapoint-restore-map.json b/tk11337-danapoint-restore-map.json
new file mode 100644
index 0000000..b00c409
--- /dev/null
+++ b/tk11337-danapoint-restore-map.json
@@ -0,0 +1,34 @@
+{
+ "ts": "2026-09-10T20:11:21.293Z",
+ "pid": "7694269251635",
+ "handle": "alys-texture-ws-split-rock-wallcovering",
+ "status": "ACTIVE",
+ "options": [
+ {
+ "name": "Size"
+ }
+ ],
+ "variants": [
+ {
+ "id": "gid://shopify/ProductVariant/43703973642291",
+ "title": "Sample",
+ "price": "4.25",
+ "sku": "DWHD-500256-Sample",
+ "policy": "CONTINUE"
+ }
+ ],
+ "mint": "XZW-511904",
+ "existing_metafield_keys": [
+ "global.description_tag",
+ "global.title_tag",
+ "global.width",
+ "dwc.width",
+ "dwc.brand",
+ "dwc.ai_generated_description",
+ "dwc.collection",
+ "dwc.contents",
+ "dwc.manufacturer_sku",
+ "dwc.real_vendor",
+ "dwc.pattern_name"
+ ]
+}
\ No newline at end of file
← 8d08f31 TK-11415: label the Momentum Meilisearch literal as a vendor
·
back to Hollywood Import
·
TK-11384: fail closed on ambiguous dw_sku in mark_shopify_pr 38dc619 →