[object Object]

← 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

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 →