[object Object]

← 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

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 →