[object Object]

← back to Designer Wallcoverings

add inventory-set-2026-newest.js — ensure newest N products' variants are tracked + on_hand=2026

cb819a0aff1f720f3a80a6ed7c2b8a10d64d8f9e · 2026-06-20 10:33:46 -0700 · Steve

Reusable scan/fix for Steve's 'last 1000 both variant inventory at 2026' rule.
First run fixed 1,617/1,915 newest variants (1,615 had tracking OFF).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit cb819a0aff1f720f3a80a6ed7c2b8a10d64d8f9e
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 20 10:33:46 2026 -0700

    add inventory-set-2026-newest.js — ensure newest N products' variants are tracked + on_hand=2026
    
    Reusable scan/fix for Steve's 'last 1000 both variant inventory at 2026' rule.
    First run fixed 1,617/1,915 newest variants (1,615 had tracking OFF).
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 shopify/scripts/inventory-set-2026-newest.js | 122 +++++++++++++++++++++++++++
 1 file changed, 122 insertions(+)

diff --git a/shopify/scripts/inventory-set-2026-newest.js b/shopify/scripts/inventory-set-2026-newest.js
new file mode 100644
index 00000000..1239e742
--- /dev/null
+++ b/shopify/scripts/inventory-set-2026-newest.js
@@ -0,0 +1,122 @@
+#!/usr/bin/env node
+/**
+ * inventory-set-2026-newest.js
+ *
+ * Ensure the newest N products (by created date) have BOTH variants at
+ * inventory on_hand = 2026, tracked + stocked at the Ventura Blvd location.
+ * Reuses cadence-import.js's exact inventorySetQuantities mechanism.
+ *
+ * Born 2026-06-20 (Steve: "make sure the last 1000 have both variant inventory
+ * at 2026"). On that first run, 1,615/1,915 newest variants had tracking OFF;
+ * this enables tracking, then sets on_hand=2026. Idempotent + re-runnable.
+ *
+ * Usage:
+ *   node inventory-set-2026-newest.js            # newest 1000, scan + fix
+ *   node inventory-set-2026-newest.js --n 500    # newest 500
+ *   node inventory-set-2026-newest.js --dry      # scan only, no writes
+ *
+ * Reads SHOPIFY_ADMIN_TOKEN from the repo-root .env (same as cadence-import.js).
+ */
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dir = path.dirname(fileURLToPath(import.meta.url));
+const ENV_PATH = path.resolve(__dir, '../../.env');     // repo root .env
+const env = fs.readFileSync(ENV_PATH, 'utf8');
+const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1]?.replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com';   // active DW store the cadence writes to
+const API = '2024-10';
+const LOC = 'gid://shopify/Location/5795643504';             // 15442 Ventura Blvd. (matches cadence-import.js)
+const QTY = 2026;
+
+const args = process.argv.slice(2);
+const flag = (k) => args.includes(k);
+const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d; };
+const N = parseInt(opt('--n', '1000'), 10);
+const DRY = flag('--dry');
+
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN in', ENV_PATH); process.exit(1); }
+const h = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+
+async function gql(q, v) {
+  for (let a = 0; a < 6; 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')) { await new Promise(s => setTimeout(s, 2500)); continue; }
+    return j;
+  }
+  return { errors: 'retry-exhausted' };
+}
+
+// ---- scan newest N ----
+const SCAN = `query($c:String){products(first:100,after:$c,sortKey:CREATED_AT,reverse:true){pageInfo{hasNextPage endCursor} nodes{id title createdAt variants(first:10){nodes{sku title inventoryItem{id tracked inventoryLevel(locationId:"${LOC}"){quantities(names:["on_hand"]){name quantity}}}}}}}}`;
+
+async function scan() {
+  let cur = null, prods = [];
+  while (prods.length < N) {
+    const j = await gql(SCAN, { c: cur });
+    const d = j.data?.products; if (!d) break;
+    prods.push(...d.nodes);
+    if (!d.pageInfo.hasNextPage) break;
+    cur = d.pageInfo.endCursor;
+  }
+  prods = prods.slice(0, N);
+  let totV = 0, correct = 0; const fixes = [];
+  for (const p of prods) for (const v of p.variants.nodes) {
+    totV++;
+    const ii = v.inventoryItem;
+    const tracked = ii?.tracked, lvl = ii?.inventoryLevel;
+    const onh = lvl?.quantities?.find(x => x.name === 'on_hand')?.quantity;
+    const reason = [];
+    if (!tracked) reason.push('untracked');
+    if (!lvl) reason.push('not-stocked@loc');
+    if (onh !== QTY) reason.push('qty=' + onh);
+    if (reason.length && ii?.id) fixes.push({ pid: p.id, sku: v.sku, iid: ii.id, tracked, stocked: !!lvl, onh, reason: reason.join('+') });
+    else if (!reason.length) correct++;
+  }
+  return { prods, totV, correct, fixes };
+}
+
+// ---- fix ----
+const INV_SET = `mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{field message code}}}`;
+
+async function fix(fixes) {
+  const untracked = fixes.filter(f => !f.tracked);
+  // 1) enable tracking — 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(`\n  tracking enabled: ${trkOk} ok, ${trkErr.length} errors ${trkErr.length ? JSON.stringify(trkErr.slice(0, 3)) : ''}`);
+  // 2) inventorySetQuantities on_hand=QTY in chunks of 200
+  const pairs = fixes.map(f => ({ inventoryItemId: f.iid, locationId: LOC, quantity: QTY }));
+  let setOk = 0; const setErr = [];
+  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 || [];
+    if (ue.length) setErr.push(...ue); setOk += chunk.length - ue.length;
+    process.stdout.write(`  set ${Math.min(i + 200, pairs.length)}/${pairs.length}\r`);
+  }
+  console.log(`\n  inventory set on_hand=${QTY}: ~${setOk} ok, ${setErr.length} errors ${setErr.length ? JSON.stringify(setErr.slice(0, 3)) : ''}`);
+}
+
+(async () => {
+  console.log(`scanning newest ${N} products on ${STORE} ...`);
+  let s = await scan();
+  console.log(`products: ${s.prods.length}  window: ${s.prods.at(-1)?.createdAt} -> ${s.prods[0]?.createdAt}`);
+  console.log(`variants: ${s.totV}  already 2026&ok: ${s.correct}  NEED FIX: ${s.fixes.length}`);
+  if (!s.fixes.length) { console.log('nothing to do.'); return; }
+  if (DRY) { console.log('--dry: no writes. sample:', JSON.stringify(s.fixes.slice(0, 5), null, 1)); return; }
+  await fix(s.fixes);
+  // verify
+  s = await scan();
+  console.log(`VERIFY -> variants: ${s.totV}  ok: ${s.correct}  remaining: ${s.fixes.length}`);
+})();

← 3f63fd9f Codify extended activation gate (specs+description+all-image  ·  back to Designer Wallcoverings  ·  activated-viewer: essentials audit + width/tag cross-check + c1e4c9db →