[object Object]

← back to Dw Rotation Activator

auto-save: 2026-07-25T12:36:15 (1 files) — cmo-activate.js

f8d965513585cb5fab024377d9b1eb48f31afa40 · 2026-07-25 12:36:20 -0700 · Steve Abrams

Files touched

Diff

commit f8d965513585cb5fab024377d9b1eb48f31afa40
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 12:36:20 2026 -0700

    auto-save: 2026-07-25T12:36:15 (1 files) — cmo-activate.js
---
 cmo-activate.js | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 75 insertions(+)

diff --git a/cmo-activate.js b/cmo-activate.js
new file mode 100644
index 0000000..033c6df
--- /dev/null
+++ b/cmo-activate.js
@@ -0,0 +1,75 @@
+// CMO-scoped settlement-gated activator. Reuses the REAL SettlementGate (legal gate)
+// + validateBeforeActivate (5-field/specs gate). Only the loop/load/activate is local.
+const fs=require('fs'), os=require('os'), path=require('path');
+const { SettlementGate } = require('./lib/settlement-gate.js');
+const { validateBeforeActivate } = require(path.join(os.homedir(),'Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js'));
+const { execSync } = require('child_process');
+const TOK=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=')[1];
+const STORE='designer-laboratory-sandbox.myshopify.com', API='2024-10';
+const GOOGLE='gid://shopify/Publication/29646651457';
+const DRY = !process.argv.includes('--commit');
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+async function gql(q,v){ for(let a=0;a<4;a++){ try{ const r=await fetch(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})}); const j=await r.json(); if(j.errors&&a<3){await sleep(1500);continue;} return j;}catch(e){await sleep(1500);} } return {}; }
+const L=fs.createWriteStream('/tmp/cmo_activate.log',{flags:'w'});
+const P=m=>{L.write(m+'\n');console.log(m);};
+
+// material map from staging (helps settlement auto-pass textures)
+const matMap={};
+for(const line of execSync(`psql "host=/tmp dbname=dw_unified" -tAF '\t' -c "SELECT mfr_sku,material FROM cmo_paris_catalog;"`).toString().trim().split('\n')){
+  const [s,m]=line.split('\t'); if(s) matMap[s]=m||'';
+}
+const mfVal=(mfs,ns,k)=>{const x=mfs.find(m=>m.namespace===ns&&m.key===k);return x?x.value:'';};
+
+async function loadPubs(){ const r=await gql(`{publications(first:50){edges{node{id name}}}}`,{}); return (r.data?.publications?.edges||[]).map(e=>e.node).filter(p=>p.id!==GOOGLE); }
+async function setHold(id,verdict,reason){ await gql(`mutation($mfs:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mfs){userErrors{message}}}`,{mfs:[{ownerId:id,namespace:'internal',key:'settlement_hold',type:'single_line_text_field',value:`${verdict}:${reason}`.slice(0,250)}]}); }
+
+(async()=>{
+  const gate=new SettlementGate(); P(`settlement lock: ${JSON.stringify(gate.lockStatus())}  mode=${DRY?'DRY':'COMMIT'}`);
+  const pubs=DRY?[]:await loadPubs(); if(!DRY)P(`publications (ex-Google): ${pubs.map(p=>p.name).join(', ')}`);
+  const Q=`query($cur:String){products(first:40,query:"vendor:'CMO Paris' status:draft",after:$cur){pageInfo{hasNextPage endCursor} nodes{id title tags descriptionHtml featuredImage{url} images(first:6){nodes{url}} variants(first:5){nodes{sku price}} metafields(first:40){nodes{namespace key value}}}}}`;
+  let cur=null, scanned=0, activated=0, publishedN=0, heldSettle=0, heldValidate=0, blocked=0, cost=0;
+  do{
+    const r=await gql(Q,{cur});
+    const pr=r.data?.products; if(!pr){P('load error: '+JSON.stringify(r).slice(0,200));break;}
+    for(const n of pr.nodes){
+      scanned++;
+      const mfs=n.metafields.nodes;
+      const dwSku=(n.tags||[]).find(t=>/^DWCX-/.test(t))||'';
+      const mfr=mfVal(mfs,'custom','manufacturer_sku');
+      const imgs=(n.images.nodes||[]).map(i=>i.url);
+      const width=mfVal(mfs,'global','width');
+      // 5-field / specs gate (real validator)
+      const val=validateBeforeActivate({title:n.title,dwSku,vendor:'CMO Paris',tags:n.tags||[],
+        descriptionHtml:n.descriptionHtml,variants:n.variants.nodes,
+        specs:{width,material:matMap[mfr]||'',repeat:'Free match'},vendorSpecs:{width,material:matMap[mfr]||''},
+        images:imgs,vendorImages:imgs});
+      // quote-only 5-field extra: sample + quotes-tag exemption + >=2 tags
+      const hasSample=(n.variants.nodes||[]).some(v=>/-sample$/i.test(v.sku||''));
+      const isQuote=(n.tags||[]).includes('quotes');
+      const extraOk=hasSample && (n.tags||[]).length>=2;
+      if(!val.ok || !extraOk){ heldValidate++; const why=[...(val.reasons||[]),...(!hasSample?['no-sample']:[]) ].join(',');
+        P(`HOLD(validate) ${dwSku} ${mfr} :: ${why}`);
+        if(!DRY) await gql(`mutation($id:ID!,$t:[String!]!){tagsAdd(id:$id,tags:$t){userErrors{message}}}`,{id:n.id,t:['Needs-Description']});
+        continue; }
+      // settlement (legal) gate
+      const sv=await gate.evaluate({shopify_id:n.id,title:n.title,vendor:'CMO Paris',dw_sku:dwSku,material:matMap[mfr]||'',imageUrl:n.featuredImage?.url||imgs[0]||''});
+      cost+=sv.cost||0;
+      if(sv.verdict!=='PASS'){ if(sv.verdict==='BLOCK')blocked++;else heldSettle++;
+        P(`HOLD(settlement:${sv.verdict}) ${dwSku} ${mfr} [${sv.tier}] :: ${sv.reason}`);
+        if(!DRY) await setHold(n.id,sv.verdict,sv.reason); continue; }
+      // PASS → activate + New Arrival + publish (ex-Google)
+      if(DRY){ activated++; continue; }
+      const ar=await gql(`mutation($id:ID!){productUpdate(input:{id:$id,status:ACTIVE}){userErrors{message}}}`,{id:n.id});
+      const ue=ar.data?.productUpdate?.userErrors||[]; if(ue.length){P(`ACTIVATE-ERR ${dwSku}: ${JSON.stringify(ue)}`);continue;}
+      activated++;
+      await gql(`mutation($id:ID!,$t:[String!]!){tagsAdd(id:$id,tags:$t){userErrors{message}}}`,{id:n.id,t:['New Arrival']});
+      const pp=await gql(`mutation($id:ID!,$in:[PublicationInput!]!){publishablePublish(id:$id,input:$in){userErrors{message}}}`,{id:n.id,in:pubs.map(p=>({publicationId:p.id}))});
+      const pe=(pp.data?.publishablePublish?.userErrors||[]).filter(e=>!/already/i.test(e.message||''));
+      if(!pe.length) publishedN++;
+      if(activated%25===0)P(`  ...activated ${activated}`);
+      await sleep(300);
+    }
+    cur=pr.pageInfo.hasNextPage?pr.pageInfo.endCursor:null;
+  }while(cur);
+  P(`\nDONE ${DRY?'(DRY)':''}: scanned=${scanned} activated=${activated} published=${publishedN} heldSettlement=${heldSettle} blocked=${blocked} heldValidate=${heldValidate}  settlement_cost=$${cost.toFixed(4)}`);
+})();

← 0b70591 rotation gate: quote-only ('quotes' tag) lines exempt from n  ·  back to Dw Rotation Activator  ·  Add mdc-activate.js: private-label MDC→Phillipe Romano activ 04ef58e →