[object Object]

← back to Designer Wallcoverings

Kravet-under-PR re-vendor tool (dry-run default; --apply/--archive gated)

080ee6528037066f774b198796ae726c91aa8b1b · 2026-07-06 14:29:28 -0700 · Steve

Files touched

Diff

commit 080ee6528037066f774b198796ae726c91aa8b1b
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 6 14:29:28 2026 -0700

    Kravet-under-PR re-vendor tool (dry-run default; --apply/--archive gated)
---
 scripts/kravet_revendor.js | 72 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 72 insertions(+)

diff --git a/scripts/kravet_revendor.js b/scripts/kravet_revendor.js
new file mode 100644
index 00000000..dbf812a5
--- /dev/null
+++ b/scripts/kravet_revendor.js
@@ -0,0 +1,72 @@
+#!/usr/bin/env node
+/**
+ * Fix Kravet products mis-vendored under "Phillipe Romano".
+ * Target = vendor:'Phillipe Romano' AND title contains 'Kravet'.
+ * Proposed change per product: vendor → 'Kravet', title → stripped of ' | Phillipe Romano...' cruft.
+ * DRY-RUN by default (enumerate only, NO writes). Pass --apply to write. --archive to archive instead of re-vendor.
+ */
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const SHOP = process.env.SHOPIFY_STORE_DOMAIN;
+const TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN;
+const VER = process.env.SHOPIFY_ADMIN_API_VERSION || '2024-01';
+const GQL = `https://${SHOP}/admin/api/${VER}/graphql.json`;
+const REST = `https://${SHOP}/admin/api/${VER}`;
+const argv = process.argv.slice(2);
+const APPLY = argv.includes('--apply');
+const ARCHIVE = argv.includes('--archive');
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
+const cleanTitle = (t) => t.replace(/\s*\|\s*Phillipe Romano.*$/i, '').trim();
+
+async function gql(query) {
+  const res = await fetch(GQL, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) });
+  const j = await res.json();
+  if (j.errors) throw new Error(JSON.stringify(j.errors));
+  return j.data;
+}
+async function rest(method, path, body) {
+  const res = await fetch(`${REST}${path}`, { method, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined });
+  if (!res.ok) throw new Error(`${res.status}: ${(await res.text()).slice(0, 200)}`);
+  return res.json();
+}
+
+(async () => {
+  // Paginate all vendor=Phillipe Romano products whose title contains Kravet.
+  const items = []; let cursor = null;
+  for (let page = 0; page < 20; page++) {
+    const after = cursor ? `, after:"${cursor}"` : '';
+    const q = `{ products(first:100${after}, query:"vendor:'Phillipe Romano' AND title:*Kravet*"){ pageInfo{hasNextPage endCursor} edges{ node{ id title status vendor variants(first:1){edges{node{sku price}}} } } } }`;
+    const d = await gql(q);
+    for (const e of d.products.edges) {
+      const n = e.node; const v = n.variants.edges[0]?.node || {};
+      items.push({ gid: n.id, id: n.id.split('/').pop(), title: n.title, status: n.status, sku: v.sku || '', price: v.price || '0' });
+    }
+    if (!d.products.pageInfo.hasNextPage) break;
+    cursor = d.products.pageInfo.endCursor; await sleep(200);
+  }
+  const nonSample = items.filter(i => !i.sku.endsWith('-Sample'));
+  const samples = items.filter(i => i.sku.endsWith('-Sample'));
+
+  const mode = ARCHIVE ? 'ARCHIVE' : 'RE-VENDOR→Kravet + strip title';
+  console.log(`${APPLY ? '' : '[DRY-RUN] '}Kravet-under-PR fix — mode: ${mode}`);
+  console.log(`Matched: ${items.length} (products ${nonSample.length}, samples ${samples.length})\n`);
+  console.log('STATUS   SKU             $PRICE     CURRENT TITLE  →  PROPOSED');
+  for (const it of nonSample) {
+    const proposed = ARCHIVE ? '(archive)' : `vendor=Kravet | "${cleanTitle(it.title)}"`;
+    console.log(`${it.status.padEnd(8)} ${it.sku.padEnd(14)} $${String(it.price).padStart(8)}  ${it.title.slice(0, 44)}  →  ${proposed}`);
+  }
+  if (samples.length) console.log(`\n(+ ${samples.length} memo-sample products would follow the same vendor/title change)`);
+
+  if (!APPLY) { console.log('\nNO WRITES PERFORMED. Re-run with --apply (or --apply --archive) to execute.'); return; }
+
+  console.log(`\nAPPLYING to ${items.length} products…`);
+  let ok = 0;
+  for (const it of items) {
+    const body = ARCHIVE ? { product: { id: Number(it.id), status: 'archived' } }
+      : { product: { id: Number(it.id), vendor: 'Kravet', title: cleanTitle(it.title) } };
+    try { await rest('PUT', `/products/${it.id}.json`, body); ok++; console.log(`  ✓ ${it.sku}`); }
+    catch (e) { console.log(`  ✗ ${it.sku}: ${e.message.slice(0, 120)}`); }
+    await sleep(350);
+  }
+  console.log(`Done ${ok}/${items.length}`);
+})().catch(e => { console.error('FATAL', e); process.exit(1); });

← a109ccd5 Carl Robinson increments: add global.Sold-Per/Minimum to ful  ·  back to Designer Wallcoverings  ·  Daisy Bennett: room-setting images (62 generated on Kamatera ee576edf →