[object Object]

← back to Dw Yolo Loop

Thibaut: strip 160 junk parenthetical tags from live store (GET-strip-PUT, preserves trailing color, 0 failed)

c695c88d59978a7b9672605163eb121271c89465 · 2026-06-12 09:41:38 -0700 · Steve Abrams

Files touched

Diff

commit c695c88d59978a7b9672605163eb121271c89465
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jun 12 09:41:38 2026 -0700

    Thibaut: strip 160 junk parenthetical tags from live store (GET-strip-PUT, preserves trailing color, 0 failed)
---
 thibaut-mfr-fix/BACKLOG.md         |  3 +-
 thibaut-mfr-fix/strip-junk-tags.js | 87 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 89 insertions(+), 1 deletion(-)

diff --git a/thibaut-mfr-fix/BACKLOG.md b/thibaut-mfr-fix/BACKLOG.md
index 3c31b1a..22ca926 100644
--- a/thibaut-mfr-fix/BACKLOG.md
+++ b/thibaut-mfr-fix/BACKLOG.md
@@ -17,7 +17,8 @@ Need the real pattern name sourced from Thibaut's catalog, then rebuild + de-SKU
 | 82254 | T8610 | T8610 Cream \| Thibaut |
 | 1244066 | TAMARAC | Tamarac \| Thibaut |
 
-## B) Junk parenthetical tags — ~160 active Thibaut products
+## B) ✅ DONE 2026-06-12 — Junk parenthetical tags stripped (160/160, 0 failed)
+_Original note:_ ~160 active Thibaut products
 Tags contain a garbage entry like `Thibaut Taluk Sisal(T41170)` (SKU embedded in a parenthetical name).
 The clean mfr token has now been added alongside; these junk tags should be stripped in a follow-up pass.
 
diff --git a/thibaut-mfr-fix/strip-junk-tags.js b/thibaut-mfr-fix/strip-junk-tags.js
new file mode 100644
index 0000000..518dcf5
--- /dev/null
+++ b/thibaut-mfr-fix/strip-junk-tags.js
@@ -0,0 +1,87 @@
+#!/usr/bin/env node
+/**
+ * Strip junk parenthetical tags ( "Thibaut Taluk Sisal(T41170)" ) from live Thibaut products.
+ * Operates on CURRENT LIVE tags (GET → strip → PUT) so it never clobbers the mfr-SKU tag
+ * added by the earlier backfill. Preserves any trailing color after the "(SKU)" (e.g. "...(T4061) - Taupe" -> "Taupe").
+ *
+ *   SHOPIFY_ADMIN_TOKEN=… node strip-junk-tags.js            # dry-run: show first 5 before/after
+ *   SHOPIFY_ADMIN_TOKEN=… node strip-junk-tags.js --apply    # PUT all, 2/sec
+ */
+const https = require('https');
+const { Pool } = require('pg');
+const STORE = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_PRODUCT_TOKEN;
+const API   = '2024-10';
+const DB    = process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp';
+const APPLY = process.argv.includes('--apply');
+if (!TOKEN) { console.error('FATAL: set SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const pool = new Pool({ connectionString: DB });
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function shopify(method, path, body) {
+  return new Promise((resolve, reject) => {
+    const data = body ? JSON.stringify(body) : null;
+    const req = https.request({ hostname: STORE, path: `/admin/api/${API}${path}`, method,
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json',
+        ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) } },
+      res => { let b=''; res.on('data',c=>b+=c); res.on('end',()=>{ let j; try{j=JSON.parse(b);}catch{j=b;}
+        resolve({ status: res.statusCode, body: j, callLimit: res.headers['x-shopify-shop-api-call-limit'] }); }); });
+    req.on('error', reject); if (data) req.write(data); req.end();
+  });
+}
+
+// remove the "(SKU)" junk from one tag segment; return cleaned text after the paren (or '' to drop)
+function cleanSegment(seg, mfr) {
+  const re = new RegExp('\\(' + mfr.replace(/[.*+?^${}()|[\]\\]/g,'\\$&') + '\\)', 'i');
+  if (!re.test(seg)) return seg;                 // not a junk segment — keep as-is
+  const after = seg.slice(seg.search(re) + seg.match(re)[0].length); // text after "(SKU)"
+  return after.replace(/^[\s\-–|]+/, '').trim(); // strip leading " - " etc; '' means drop
+}
+
+function rebuildTags(tags, mfr) {
+  const out = []; const seen = new Set();
+  for (const raw of tags.split(',')) {
+    const seg = raw.trim(); if (!seg) continue;
+    const cleaned = cleanSegment(seg, mfr);
+    if (!cleaned) continue;                       // dropped junk
+    const k = cleaned.toLowerCase();
+    if (seen.has(k)) continue;                    // dedup
+    seen.add(k); out.push(cleaned);
+  }
+  return out.join(', ');
+}
+
+(async () => {
+  const { rows } = await pool.query(`
+    SELECT split_part(shopify_id,'/',5) AS pid, mfr_sku
+    FROM shopify_products
+    WHERE (vendor_prefix='DWTT' OR vendor ILIKE 'thibaut') AND mfr_sku IS NOT NULL AND mfr_sku<>''
+      AND status='ACTIVE' AND shopify_id ~ '^gid://shopify/Product/[0-9]+$'
+      AND tags ~ ('\\(' || mfr_sku || '\\)')
+    ORDER BY pid`);
+  console.log(`Store: ${STORE} · junk-tag products: ${rows.length} · mode: ${APPLY ? 'APPLY' : 'DRY-RUN'}`);
+  let ok=0, fail=0, nochange=0, shown=0;
+  for (const r of rows) {
+    let live;
+    try { live = await shopify('GET', `/products/${r.pid}.json?fields=id,tags`); }
+    catch (e) { fail++; console.error(`  GET ERR ${r.pid}: ${e.message}`); await sleep(520); continue; }
+    if (live.status !== 200) { fail++; console.error(`  GET ${r.pid}: HTTP ${live.status}`); await sleep(520); continue; }
+    const before = live.body.product.tags || '';
+    const after = rebuildTags(before, r.mfr_sku);
+    if (after === before) { nochange++; continue; }
+    if (!APPLY) {
+      if (shown++ < 5) {
+        const removed = before.split(',').map(s=>s.trim()).filter(s => !after.split(', ').includes(s));
+        console.log(`\n  ${r.pid} (mfr ${r.mfr_sku}) removed/changed: ${JSON.stringify(removed)}`);
+      }
+      await sleep(120); continue;
+    }
+    const res = await shopify('PUT', `/products/${r.pid}.json`, { product: { id: Number(r.pid), tags: after } });
+    if (res.status === 200) ok++; else { fail++; console.error(`  PUT FAIL ${r.pid}: HTTP ${res.status} ${JSON.stringify(res.body).slice(0,120)}`); }
+    if ((ok+fail) % 50 === 0) console.log(`  ...${ok+fail}/${rows.length} (ok=${ok} fail=${fail})`);
+    const [used] = (res.callLimit || '0/40').split('/').map(Number);
+    await sleep(used > 30 ? 1500 : 520);
+  }
+  console.log(`\n${APPLY ? 'DONE' : 'DRY-RUN'}. changed=${ok} failed=${fail} no-change=${nochange} of ${rows.length}`);
+  await pool.end();
+})().catch(e => { console.error(e); process.exit(1); });

← 7a52550 Artmura site: README documenting features, data, endpoints,  ·  back to Dw Yolo Loop  ·  Artmura push: fix 422 (drop metafields from create — store d 55fc397 →