← back to Designer Wallcoverings
add sweep-all-active.mjs — full-catalog inventory=2026 backfill
86f2a444c2844d725ece0a67471a04147720522a · 2026-06-22 13:46:32 -0700 · Steve
On-demand sweep over the ENTIRE active catalog (companion to the hourly
newest-1000 cadence hook). First run set 130,760 active variants to
tracked + on_hand=2026 (8,274 tracking enables, 0 errors). Notes the
productsCount 10k cap so future runs size off pagination, not the count.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
A shopify/scripts/sweep-all-active.mjs
Diff
commit 86f2a444c2844d725ece0a67471a04147720522a
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jun 22 13:46:32 2026 -0700
add sweep-all-active.mjs — full-catalog inventory=2026 backfill
On-demand sweep over the ENTIRE active catalog (companion to the hourly
newest-1000 cadence hook). First run set 130,760 active variants to
tracked + on_hand=2026 (8,274 tracking enables, 0 errors). Notes the
productsCount 10k cap so future runs size off pagination, not the count.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
shopify/scripts/sweep-all-active.mjs | 78 ++++++++++++++++++++++++++++++++++++
1 file changed, 78 insertions(+)
diff --git a/shopify/scripts/sweep-all-active.mjs b/shopify/scripts/sweep-all-active.mjs
new file mode 100644
index 00000000..415ad4dc
--- /dev/null
+++ b/shopify/scripts/sweep-all-active.mjs
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+/**
+ * sweep-all-active.mjs
+ *
+ * One-time / on-demand FULL-CATALOG inventory backfill: ensure EVERY active
+ * product has both variants tracked + on_hand = 2026 at the Ventura Blvd
+ * location. Companion to inventory-set-2026-newest.mjs (which the hourly
+ * cadence runs over only the newest 1000) — this one sweeps the entire
+ * active catalog.
+ *
+ * Born 2026-06-22 (Steve: "add inventory to all cork- active items, then make
+ * sure ALL active products have 2026 inventory"). First run touched ~75k active
+ * products / 130,760 variants (8,274 had tracking OFF; all set to 2026, 0 errors).
+ *
+ * NOTE: Shopify's productsCount caps its return at 10,000 — do NOT trust it for
+ * sizing; this script paginates the real catalog (status:active) page by page.
+ *
+ * Idempotent + cap-free (inventory sets don't consume the daily variant cap).
+ * Reads SHOPIFY_ADMIN_TOKEN from env first (cadence exports it), else repo .env.
+ *
+ * Usage: node shopify/scripts/sweep-all-active.mjs
+ * Writes a run summary to /tmp/sweep-summary.json.
+ */
+import fs from 'fs';
+const env=fs.readFileSync('.env','utf8');
+const TOKEN=process.env.SHOPIFY_ADMIN_TOKEN||(env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m)||[])[1].replace(/['"]/g,'').trim();
+const STORE='designer-laboratory-sandbox.myshopify.com',API='2024-10';
+const LOC='gid://shopify/Location/5795643504';
+const h={'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'};
+async function gql(q,v){for(let a=0;a<8;a++){const r=await fetch(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:h,body:JSON.stringify({query:q,variables:v})});const j=await r.json();if((j.errors&&JSON.stringify(j.errors).includes('Throttled'))||(j.errors&&JSON.stringify(j.errors).includes('throttled'))){await new Promise(s=>setTimeout(s,3000));continue;}return j;}return{errors:'retry-exhausted'};}
+
+// 1) scan ALL active products → variant inventoryItem id + tracked
+const SCAN=`query($c:String){products(first:40,after:$c,query:"status:active"){pageInfo{hasNextPage endCursor} nodes{variants(first:15){nodes{sku inventoryItem{id tracked}}}}}}`;
+let cur=null,vars=[],pages=0;
+while(true){const j=await gql(SCAN,{c:cur});const d=j.data?.products;if(!d){console.log('scan error',JSON.stringify(j.errors).slice(0,200));break;}
+ for(const p of d.nodes)for(const v of p.variants.nodes){if(v.inventoryItem?.id)vars.push({iid:v.inventoryItem.id,sku:v.sku||'',tracked:v.inventoryItem.tracked});}
+ pages++;if(pages%25===0)process.stdout.write(` scan ${vars.length} variants (${pages} pages)\r`);
+ if(!d.pageInfo.hasNextPage)break;cur=d.pageInfo.endCursor;}
+const cork=vars.filter(v=>/^cork-/i.test(v.sku));
+const untracked=vars.filter(v=>!v.tracked);
+console.log(`\nscanned: ${vars.length} active variants (${pages} pages)`);
+console.log(` cork- SKUs: ${cork.length} variants | untracked (need tracking on): ${untracked.length}`);
+
+// 2) enable tracking on untracked — aliased batches of 20
+let trkOk=0;const trkErr=[];
+for(let i=0;i<untracked.length;i+=20){const batch=untracked.slice(i,i+20);
+ const m=batch.map((f,j)=>`m${j}:inventoryItemUpdate(id:"${f.iid}",input:{tracked:true}){userErrors{message}}`).join('\n');
+ const r=await gql(`mutation{${m}}`,{});const d=r.data||{};
+ for(let j=0;j<batch.length;j++){const ue=d[`m${j}`]?.userErrors||[];if(ue.length)trkErr.push(...ue);else trkOk++;}
+ process.stdout.write(` track ${Math.min(i+20,untracked.length)}/${untracked.length}\r`);}
+if(untracked.length)console.log(`\ntracking enabled: ${trkOk} ok, ${trkErr.length} errors ${trkErr.length?JSON.stringify(trkErr.slice(0,3)):''}`);
+
+// 3) set on_hand=2026 on ALL active variants — chunks of 200
+const INV_SET=`mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{field message code}}}`;
+const pairs=vars.map(v=>({inventoryItemId:v.iid,locationId:LOC,quantity:2026}));
+let setOk=0;const notStocked=[];const otherErr=[];
+for(let i=0;i<pairs.length;i+=200){const chunk=pairs.slice(i,i+200);
+ const r=await gql(INV_SET,{input:{name:'on_hand',reason:'correction',ignoreCompareQuantity:true,quantities:chunk}});
+ const ue=r.data?.inventorySetQuantities?.userErrors||[];
+ setOk+=chunk.length-ue.length;
+ if(ue.length){ // map errors back to inventoryItemIds by field path index
+ for(const e of ue){const mi=/quantities\.(\d+)/.exec((e.field||[]).join?.('.')||JSON.stringify(e.field||''));const li=mi?+mi[1]:null;const item=li!=null?chunk[li]:null;
+ if(/not stocked|does not have inventory|inventory item.*location/i.test(e.message||''))notStocked.push(item?.inventoryItemId);else otherErr.push(e.message);}}
+ process.stdout.write(` set ${Math.min(i+200,pairs.length)}/${pairs.length}\r`);}
+console.log(`\ninventory set on_hand=2026: ~${setOk}/${pairs.length} ok notStocked=${notStocked.filter(Boolean).length} otherErr=${otherErr.length} ${otherErr.length?JSON.stringify(otherErr.slice(0,3)):''}`);
+
+// 4) activate any not-stocked items at the location, then set
+const ns=notStocked.filter(Boolean);
+if(ns.length){console.log(`activating ${ns.length} not-stocked items at Ventura Blvd, then setting...`);
+ const ACT=`mutation($id:ID!,$loc:ID!){inventoryActivate(inventoryItemId:$id,locationId:$loc,available:2026){userErrors{message}}}`;
+ let actOk=0;for(const id of ns){const r=await gql(ACT,{id,loc:LOC});if(!(r.data?.inventoryActivate?.userErrors||[]).length)actOk++;}
+ // re-set on_hand for those
+ const p2=ns.map(id=>({inventoryItemId:id,locationId:LOC,quantity:2026}));
+ for(let i=0;i<p2.length;i+=200){await gql(INV_SET,{input:{name:'on_hand',reason:'correction',ignoreCompareQuantity:true,quantities:p2.slice(i,i+200)}});}
+ console.log(` activated ${actOk}/${ns.length}, re-set on_hand=2026`);}
+
+fs.writeFileSync('/tmp/sweep-summary.json',JSON.stringify({variants:vars.length,cork:cork.length,untracked:untracked.length,setOk,notStocked:ns.length,otherErr:otherErr.length}));
+console.log('DONE');
← 796b1a30 Tier B MOQ: read-only UOM packaging scan + per-vendor propos
·
back to Designer Wallcoverings
·
Transform collection filters from vertical sidebar to horizo 837f4d8c →