← 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
A shopify/scripts/inventory-set-2026-newest.js
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 →