[object Object]

← back to Designer Wallcoverings

Remap reseller vendor 'Newwall' -> real brand per-SKU (Coordonné 46 / Tres Tintas 28), scrub leak from vendor+title+description on 74 products

9a06a6a093d252d5d7bf3159fc1996b695b3da8a · 2026-06-26 09:25:22 -0700 · Steve

Files touched

Diff

commit 9a06a6a093d252d5d7bf3159fc1996b695b3da8a
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jun 26 09:25:22 2026 -0700

    Remap reseller vendor 'Newwall' -> real brand per-SKU (Coordonné 46 / Tres Tintas 28), scrub leak from vendor+title+description on 74 products
---
 shopify/scripts/remap-newwall-to-coordonne.js | 106 ++++++++++++++++++++++++++
 1 file changed, 106 insertions(+)

diff --git a/shopify/scripts/remap-newwall-to-coordonne.js b/shopify/scripts/remap-newwall-to-coordonne.js
new file mode 100644
index 00000000..3bbb6cfe
--- /dev/null
+++ b/shopify/scripts/remap-newwall-to-coordonne.js
@@ -0,0 +1,106 @@
+#!/usr/bin/env node
+/**
+ * Remap vendor "Newwall" (a reseller) -> real brand "Coordonné" on the 23 leaked
+ * DWXW products. Scrubs the leak from 3 fields: vendor, title suffix, descriptionHtml.
+ * Source of truth for the real brand: dw_unified.newwall_catalog.vendor_name
+ * (verified 2026-06-26: all 23 -> Coordonné).
+ *
+ * DRY RUN by default. Set APPLY=1 to write to Shopify + sync local mirror.
+ */
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+
+const ROOT = path.resolve(__dirname, '..', '..');
+const env = Object.fromEntries(
+  fs.readFileSync(path.join(ROOT, '.env'), 'utf8')
+    .split('\n').filter(l => l.includes('=') && !l.trim().startsWith('#'))
+    .map(l => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()]; })
+);
+
+const STORE = (env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com').replace(/^https?:\/\//, '');
+const TOKEN = env.SHOPIFY_ADMIN_TOKEN || env.SHOPIFY_ADMIN_ACCESS_TOKEN;
+const APPLY = process.env.APPLY === '1';
+
+if (!TOKEN) { console.error('No SHOPIFY_ADMIN_TOKEN in .env'); process.exit(1); }
+
+// Per-SKU real-brand map (DWXW-key -> real brand) from dw_unified.newwall_catalog.vendor_name.
+// newwall.com is a reseller; the 74 live products split Coordonné(46)/Tres Tintas(28).
+const BRAND_MAP = Object.fromEntries(
+  fs.readFileSync('/tmp/newwall_brand_map.tsv', 'utf8').trim().split('\n')
+    .map(l => l.split('\t')).map(([k, v]) => [k.toUpperCase(), v])
+);
+const keyOf = handle => { const m = (handle || '').match(/dwxw-\d+/i); return m ? m[0].toUpperCase() : null; };
+
+async function gql(query, variables) {
+  const r = await fetch(`https://${STORE}/admin/api/2024-01/graphql.json`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN },
+    body: JSON.stringify({ query, variables }),
+  });
+  return r.json();
+}
+
+// --- field transforms (brand passed per-product) ---
+const fixTitle = (t, brand) => t.replace(/\s*\|\s*Newwall\s*$/i, ` | ${brand}`).replace(/Newwall/gi, brand);
+const fixDesc = (d, brand) => (d || '')
+  .replace(/from the\s+[A-Za-z0-9]+\s+collection by\s+Newwall/gi, `by ${brand}`) // kills bogus "C collection"
+  .replace(/Newwall/gi, brand);
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+async function fetchAll() {
+  const out = [];
+  let cursor = null, more = true;
+  while (more) {
+    const res = await gql(`
+      query($c:String){ products(first:100, query:"vendor:'Newwall'", after:$c){
+        pageInfo{hasNextPage endCursor}
+        edges{ node{ id handle title vendor descriptionHtml status } } } }`, { c: cursor });
+    const edges = res.data?.products?.edges || [];
+    edges.forEach(e => out.push(e.node));
+    const pi = res.data?.products?.pageInfo; more = pi?.hasNextPage; cursor = pi?.endCursor;
+  }
+  return out;
+}
+
+(async () => {
+  const products = await fetchAll();
+  console.log(`Found ${products.length} live products with vendor "Newwall"\n${'='.repeat(70)}`);
+  let changed = 0, skipped = 0;
+  const tally = {};
+  for (const p of products) {
+    const brand = BRAND_MAP[keyOf(p.handle)];
+    if (!brand) { skipped++; console.log(`\n[SKIP no brand match] ${p.handle}`); continue; }
+    const newTitle = fixTitle(p.title, brand);
+    const newDesc = fixDesc(p.descriptionHtml, brand);
+    const willChange = p.vendor !== brand || newTitle !== p.title || newDesc !== p.descriptionHtml;
+    if (!willChange) continue;
+    changed++; tally[brand] = (tally[brand] || 0) + 1;
+    console.log(`\n[${p.status}] ${p.handle}`);
+    console.log(`  vendor: ${p.vendor}  ->  ${brand}`);
+    if (newTitle !== p.title) console.log(`  title : ${p.title}\n        -> ${newTitle}`);
+    if (newDesc !== p.descriptionHtml) console.log(`  desc  : …Newwall… -> …${brand}… (collection-letter artifact removed)`);
+
+    if (APPLY) {
+      const r = await gql(`
+        mutation($in:ProductInput!){ productUpdate(input:$in){ product{id} userErrors{field message} } }`,
+        { in: { id: p.id, title: newTitle, vendor: brand, descriptionHtml: newDesc } });
+      const errs = r.data?.productUpdate?.userErrors || [];
+      if (errs.length) { console.log(`  !! ERROR: ${JSON.stringify(errs)}`); }
+      else {
+        console.log(`  ✓ written`);
+        // keep local mirror in sync (PG is source of truth per CLAUDE.md; this re-aligns the cache)
+        try {
+          execFileSync('psql', ['-d', 'dw_unified', '-c',
+            `update shopify_products set vendor=$$${brand}$$, title=$$${newTitle.replace(/\$/g,'')}$$ where handle=$$${p.handle}$$`],
+            { stdio: 'ignore' });
+        } catch (_) {}
+      }
+      await sleep(350); // rate-limit courtesy
+    }
+  }
+  console.log(`\n${'='.repeat(70)}\n${APPLY ? 'APPLIED' : 'DRY RUN'} — ${changed}/${products.length} changed, ${skipped} skipped.`);
+  console.log('Per-brand:', JSON.stringify(tally));
+  if (!APPLY) console.log('Re-run with APPLY=1 to write live.');
+})();

← 9e5acb6f auto-save: 2026-06-26T08:43:59 (38 files) — pending-approval  ·  back to Designer Wallcoverings  ·  auto-save: 2026-06-26T09:44:31 (5 files) — pending-approval/ 14755f27 →