← back to Designer Wallcoverings
Store-wide sweep: set every active variant inventory=2026 + publish to all 13 sales channels (idempotent, resumable)
3f4ec431832137ef7d4078fe4b986b34c255ef34 · 2026-06-11 11:26:51 -0700 · SteveStudio2
Files touched
A shopify/scripts/.gitignoreA shopify/scripts/active-2026-and-all-channels.js
Diff
commit 3f4ec431832137ef7d4078fe4b986b34c255ef34
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Thu Jun 11 11:26:51 2026 -0700
Store-wide sweep: set every active variant inventory=2026 + publish to all 13 sales channels (idempotent, resumable)
---
shopify/scripts/.gitignore | 1 +
shopify/scripts/active-2026-and-all-channels.js | 127 ++++++++++++++++++++++++
2 files changed, 128 insertions(+)
diff --git a/shopify/scripts/.gitignore b/shopify/scripts/.gitignore
new file mode 100644
index 00000000..500c8174
--- /dev/null
+++ b/shopify/scripts/.gitignore
@@ -0,0 +1 @@
+data/active-2026-sweep/
diff --git a/shopify/scripts/active-2026-and-all-channels.js b/shopify/scripts/active-2026-and-all-channels.js
new file mode 100644
index 00000000..1e472a6e
--- /dev/null
+++ b/shopify/scripts/active-2026-and-all-channels.js
@@ -0,0 +1,127 @@
+#!/usr/bin/env node
+/**
+ * Store-wide enforcement sweep (Steve 2026-06-11):
+ * "every active item must have ALL 13 sales channels AND 2026 inventory for every variant."
+ *
+ * For each ACTIVE product:
+ * - publish to any of the 13 sales-channel publications it's missing (idempotent).
+ * - set AVAILABLE inventory = 2026 for EVERY variant at the Ventura Blvd location,
+ * via batched inventorySetQuantities (250/call, ignoreCompareQuantity). Variants not
+ * yet stocked at the location are activated at 2026 (inventoryActivate fallback).
+ *
+ * node active-2026-and-all-channels.js --dry-run [--limit N]
+ * node active-2026-and-all-channels.js --apply [--limit N]
+ *
+ * Idempotent + resumable: re-running only fixes what's still off. Progress + failures
+ * logged to data/active-2026-sweep/.
+ */
+const https = require('https');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const args = process.argv.slice(2);
+const APPLY = args.includes('--apply');
+const LIMIT = (() => { const i = args.indexOf('--limit'); return i >= 0 ? parseInt(args[i + 1], 10) : Infinity; })();
+const TARGET_QTY = 2026;
+const LOCATION_ID = 'gid://shopify/Location/5795643504'; // 15442 Ventura Blvd.
+const SET_BATCH = 250;
+
+const env = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+const PUBS = [
+ ['Online Store', '22208643184'], ['Buy Button', '22497296496'], ['Google & YouTube', '29646651457'],
+ ['Facebook & Instagram', '29739483201'], ['Houzz', '29776969793'], ['Point of Sale', '37904089153'],
+ ['Fabricut', '43657658419'], ['Pinterest', '44234276915'], ['Rakuten Ichiba (JP)', '44317474867'],
+ ['Shop', '44317507635'], ['Inbox', '71898464307'], ['TikTok', '115856375859'], ['DWAutoPostBlog', '140027723827'],
+].map(([name, id]) => ({ name, gid: `gid://shopify/Publication/${id}`, alias: 'p' + id }));
+const PUB_ALIASES = PUBS.map(p => `${p.alias}:publishedOnPublication(publicationId:"${p.gid}")`).join(' ');
+
+function gql(query, variables = {}) {
+ return new Promise((resolve, reject) => {
+ const body = JSON.stringify({ query, variables });
+ const req = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN, 'Content-Length': Buffer.byteLength(body) } },
+ r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(d); } }); });
+ req.on('error', reject); req.write(body); req.end();
+ });
+}
+async function gqlR(q, v = {}, tries = 5) {
+ for (let i = 0; i < tries; i++) {
+ let r; try { r = await gql(q, v); } catch (e) { r = { errors: [{ message: String(e).slice(0, 120) }] }; }
+ if (r && !r.errors) return r;
+ await sleep(1500 * (i + 1)); // throttle/backoff
+ }
+ return gql(q, v);
+}
+
+const M_PUBLISH = `mutation($id:ID!,$input:[PublicationInput!]!){publishablePublish(id:$id,input:$input){userErrors{field message}}}`;
+const M_SET = `mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{field message}}}`;
+const M_ACTIVATE = `mutation($itemId:ID!,$locId:ID!,$qty:Int!){inventoryActivate(inventoryItemId:$itemId,locationId:$locId,available:$qty){userErrors{field message}}}`;
+
+const OUTDIR = path.join(__dirname, 'data', 'active-2026-sweep');
+fs.mkdirSync(OUTDIR, { recursive: true });
+const FAILLOG = path.join(OUTDIR, 'failures.jsonl');
+const logFail = o => fs.appendFileSync(FAILLOG, JSON.stringify(o) + '\n');
+
+let invBuf = []; // {inventoryItemId, locationId, quantity}
+let stats = { products: 0, published: 0, pubErrors: 0, invSet: 0, invActivated: 0, invErrors: 0 };
+
+async function flushInv() {
+ if (!invBuf.length) return;
+ const batch = invBuf; invBuf = [];
+ const r = await gqlR(M_SET, { input: { reason: 'correction', name: 'available', ignoreCompareQuantity: true, quantities: batch } });
+ const ue = r.data?.inventorySetQuantities?.userErrors || [];
+ if (!ue.length) { stats.invSet += batch.length; return; }
+ // Some items not stocked at location → activate them individually at 2026.
+ for (const q of batch) {
+ const ar = await gqlR(M_ACTIVATE, { itemId: q.inventoryItemId, locId: LOCATION_ID, qty: TARGET_QTY });
+ const ae = ar.data?.inventoryActivate?.userErrors || [];
+ if (ae.length) { stats.invErrors++; logFail({ inventoryItemId: q.inventoryItemId, ae }); }
+ else stats.invActivated++;
+ await sleep(60);
+ }
+}
+
+(async () => {
+ console.log(`active-2026-and-all-channels — ${APPLY ? '🔴 LIVE' : 'DRY-RUN'} — target qty ${TARGET_QTY} @ Ventura Blvd, all 13 channels\n`);
+ let cursor = null, page = 0;
+ outer: while (true) {
+ const q = `query($c:String){products(first:25,after:$c,query:"status:active"){pageInfo{hasNextPage endCursor}
+ nodes{id ${PUB_ALIASES} variants(first:50){nodes{id inventoryItem{id}}}}}}`;
+ const r = await gqlR(q, { c: cursor });
+ if (r.errors) { console.log('PAGE ERR', JSON.stringify(r.errors).slice(0, 200)); break; }
+ const pg = r.data.products;
+ for (const n of pg.nodes) {
+ if (stats.products >= LIMIT) break outer;
+ stats.products++;
+ // channels
+ const missing = PUBS.filter(p => !n[p.alias]).map(p => ({ publicationId: p.gid }));
+ if (missing.length) {
+ if (APPLY) {
+ const pr = await gqlR(M_PUBLISH, { id: n.id, input: missing });
+ const ue = pr.data?.publishablePublish?.userErrors || [];
+ if (ue.length) { stats.pubErrors++; logFail({ product: n.id, pubErrors: ue }); }
+ else stats.published++;
+ } else stats.published++;
+ }
+ // inventory: every variant → 2026
+ for (const v of n.variants.nodes) {
+ if (!v.inventoryItem?.id) continue;
+ if (APPLY) { invBuf.push({ inventoryItemId: v.inventoryItem.id, locationId: LOCATION_ID, quantity: TARGET_QTY });
+ if (invBuf.length >= SET_BATCH) await flushInv(); }
+ else stats.invSet++;
+ }
+ if (stats.products % 500 === 0) console.log(` …${stats.products} products | published+${stats.published} | invSet ${stats.invSet} activated ${stats.invActivated} | errs pub ${stats.pubErrors} inv ${stats.invErrors}`);
+ }
+ page++;
+ if (!pg.pageInfo.hasNextPage) break;
+ cursor = pg.pageInfo.endCursor;
+ }
+ if (APPLY) await flushInv();
+ console.log(`\nDONE. products=${stats.products} channelsPublished=${stats.published} invSet=${stats.invSet} invActivated=${stats.invActivated} pubErrors=${stats.pubErrors} invErrors=${stats.invErrors}`);
+ console.log(`failures (if any) → ${FAILLOG}`);
+})();
← 8eb73344 romo roll pricing: RFC-4180 CSV parser (quoted fields w/ com
·
back to Designer Wallcoverings
·
Kravet-family feed importer (authorized daily SFTP, no scrap ef219ca4 →