[object Object]

← back to Designer Wallcoverings

Quadrille family reclassification: 3 Alan Campbell + 24 small-line lines -> Fabric; China Seas kept residential Wallcovering (reverted catalog Commercial-WC flag per Steve)

a324b3e2ec81eb4db91deead884a91c1f7d1d9e8 · 2026-06-30 09:25:33 -0700 · Steve

Files touched

Diff

commit a324b3e2ec81eb4db91deead884a91c1f7d1d9e8
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jun 30 09:25:33 2026 -0700

    Quadrille family reclassification: 3 Alan Campbell + 24 small-line lines -> Fabric; China Seas kept residential Wallcovering (reverted catalog Commercial-WC flag per Steve)
---
 shopify/scripts/china-seas-revert-commercial.js | 61 +++++++++++++++
 shopify/scripts/quadrille-type-fix.js           | 98 +++++++++++++++++++++++++
 2 files changed, 159 insertions(+)

diff --git a/shopify/scripts/china-seas-revert-commercial.js b/shopify/scripts/china-seas-revert-commercial.js
new file mode 100644
index 00000000..b879d108
--- /dev/null
+++ b/shopify/scripts/china-seas-revert-commercial.js
@@ -0,0 +1,61 @@
+#!/usr/bin/env node
+/**
+ * china-seas-revert-commercial.js  (one-off, 2026-06-30)
+ * Steve: China Seas is residential — Fabrics and Wallpaper only, NO Commercial Wallcovering.
+ * Revert the 52 China Seas products that were set to 'Commercial Wallcovering' back to
+ * 'Wallcovering' (wallpaper), removing the stray 'Commercial Wallcovering' tag, on
+ * live Shopify AND the dw_unified mirror.
+ *
+ * Usage: node china-seas-revert-commercial.js --dry | node china-seas-revert-commercial.js
+ */
+const { Pool } = require('pg');
+require('dotenv').config({ path: __dirname + '/../.env' });
+
+const DRY = process.argv.includes('--dry');
+const SHOP = process.env.SHOPIFY_SHOP_URL || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ADMIN_API_TOKEN;
+const pool = process.env.DATABASE_URL
+  ? new Pool({ connectionString: process.env.DATABASE_URL })
+  : new Pool({ host: '/tmp', database: 'dw_unified', user: process.env.PGUSER || process.env.USER });
+
+function normalizeTags(tagStr) {
+  let tags = (tagStr || '').split(',').map(t => t.trim()).filter(Boolean);
+  tags = tags.filter(t => t !== 'Commercial Wallcovering');     // drop commercial
+  if (!tags.includes('Wallcovering')) tags.push('Wallcovering'); // ensure wallpaper tag
+  return [...new Set(tags)].join(', ');
+}
+async function api(method, path, body) {
+  const res = await fetch(`https://${SHOP}/admin/api/2024-10/${path}`, {
+    method, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+    body: body ? JSON.stringify(body) : undefined });
+  if (!res.ok) throw new Error(`${method} ${path} -> ${res.status} ${await res.text()}`);
+  return res.json();
+}
+
+(async () => {
+  // the 52: china_seas_catalog rows flagged Commercial Wallcovering, linked to Shopify
+  const { rows } = await pool.query(`
+    SELECT DISTINCT regexp_replace(shopify_product_id,'\\D','','g') AS pid
+    FROM china_seas_catalog WHERE product_type='Commercial Wallcovering' AND shopify_product_id ~ '\\d'`);
+  console.log(`Reverting ${rows.length} China Seas products -> Wallcovering  (${DRY ? 'DRY' : 'EXECUTE'})`);
+  let ok = 0, fail = 0;
+  for (const { pid } of rows) {
+    try {
+      if (DRY) { console.log(`  [dry] ${pid}: type->Wallcovering, drop Commercial Wallcovering tag`); ok++; continue; }
+      const live = (await api('GET', `products/${pid}.json?fields=id,tags`)).product;
+      await api('PUT', `products/${pid}.json`, { product: { id: Number(pid), product_type: 'Wallcovering', tags: normalizeTags(live.tags) } });
+      await pool.query(`
+        UPDATE shopify_products
+        SET product_type='Wallcovering',
+            tags = array_to_string(
+                     (SELECT array_agg(DISTINCT x) FROM (
+                        SELECT trim(t) AS x FROM unnest(string_to_array(tags, ',')) t
+                        WHERE trim(t) <> '' AND trim(t) <> 'Commercial Wallcovering'
+                        UNION SELECT 'Wallcovering') s WHERE x<>''), ', ')
+        WHERE regexp_replace(shopify_id,'\\D','','g') = $1`, [pid]);
+      console.log(`  ✓ ${pid}`); ok++;
+    } catch (e) { console.error(`  ✗ ${pid}: ${e.message}`); fail++; }
+  }
+  console.log(`\nDone. ok=${ok} fail=${fail}`);
+  await pool.end();
+})();
diff --git a/shopify/scripts/quadrille-type-fix.js b/shopify/scripts/quadrille-type-fix.js
new file mode 100644
index 00000000..3805a9ca
--- /dev/null
+++ b/shopify/scripts/quadrille-type-fix.js
@@ -0,0 +1,98 @@
+#!/usr/bin/env node
+/**
+ * quadrille-type-fix.js  (one-off, 2026-06-30)
+ * Push correct product_type + type-tag from the unified catalog truth to live Shopify
+ * for mis-typed Quadrille-family products, then sync the dw_unified mirror.
+ *
+ *  - 24 small-line products (Suncloth/Quadrille/Home Couture/Plains/Charles Burger/
+ *    Cloth and Paper):  Wallcovering -> Fabric                 (catalog: quadrille_house_catalog)
+ *  - 52 China Seas products:          Wallcovering -> Commercial Wallcovering (catalog: china_seas_catalog)
+ *
+ * Type+tag only. All small-line products are sample-only (no unit/MOQ cascade).
+ * Re-fetches live tags per product and does a token-level swap. Idempotent.
+ *
+ * Usage:  node quadrille-type-fix.js --dry    # show plan, no writes
+ *         node quadrille-type-fix.js          # execute
+ */
+const { Pool } = require('pg');
+require('dotenv').config({ path: __dirname + '/../.env' });
+
+const DRY = process.argv.includes('--dry');
+const SHOP = process.env.SHOPIFY_SHOP_URL || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ADMIN_API_TOKEN;
+const pool = process.env.DATABASE_URL
+  ? new Pool({ connectionString: process.env.DATABASE_URL })
+  : new Pool({ host: '/tmp', database: 'dw_unified', user: process.env.PGUSER || process.env.USER });
+
+// tag-token swap helper
+function swapTag(tagStr, from, to) {
+  const tags = (tagStr || '').split(',').map(t => t.trim()).filter(Boolean);
+  const out = tags.map(t => (t === from ? to : t));
+  return [...new Set(out)].join(', ');
+}
+
+async function api(method, path, body) {
+  const res = await fetch(`https://${SHOP}/admin/api/2024-10/${path}`, {
+    method,
+    headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+    body: body ? JSON.stringify(body) : undefined,
+  });
+  if (!res.ok) throw new Error(`${method} ${path} -> ${res.status} ${await res.text()}`);
+  return res.json();
+}
+
+// Build the target set from catalog truth joined to the live mirror.
+const TARGETS_SQL = `
+  WITH src AS (
+    SELECT regexp_replace(shopify_product_id,'\\D','','g') AS pid, product_type AS truth
+      FROM china_seas_catalog WHERE shopify_product_id ~ '\\d'
+    UNION ALL
+    SELECT regexp_replace(shopify_product_id,'\\D','','g'), product_type
+      FROM quadrille_house_catalog WHERE shopify_product_id ~ '\\d'
+  ),
+  m AS (SELECT regexp_replace(shopify_id,'\\D','','g') AS pid, shopify_id, vendor, product_type AS live, tags
+          FROM shopify_products)
+  SELECT m.pid, m.shopify_id, m.vendor, m.live, src.truth, m.tags
+    FROM src JOIN m USING (pid)
+   WHERE src.truth <> m.live
+     -- only the family corrections we agreed to:
+     AND ( (src.truth='Fabric' AND m.live='Wallcovering')
+        OR (src.truth='Commercial Wallcovering' AND m.live='Wallcovering') )
+   ORDER BY m.vendor;`;
+
+(async () => {
+  const { rows } = await pool.query(TARGETS_SQL);
+  console.log(`Targets: ${rows.length} products  (${DRY ? 'DRY RUN' : 'EXECUTING'})`);
+  const byType = {};
+  rows.forEach(r => { const k = `${r.live} -> ${r.truth}`; byType[k] = (byType[k]||0)+1; });
+  console.log(byType);
+
+  let ok = 0, fail = 0;
+  for (const r of rows) {
+    const fromTag = 'Wallcovering';
+    const toTag = r.truth; // 'Fabric' or 'Commercial Wallcovering'
+    try {
+      if (DRY) {
+        console.log(`  [dry] ${r.vendor} ${r.pid}: type->${r.truth}, tag ${fromTag}->${toTag}`);
+        ok++; continue;
+      }
+      // re-fetch live tags
+      const live = (await api('GET', `products/${r.pid}.json?fields=id,tags,product_type`)).product;
+      const newTags = swapTag(live.tags, fromTag, toTag);
+      await api('PUT', `products/${r.pid}.json`, { product: { id: Number(r.pid), product_type: r.truth, tags: newTags } });
+      // sync mirror
+      await pool.query(
+        `UPDATE shopify_products SET product_type=$1,
+           tags = regexp_replace(tags, '(^|, )Wallcovering(,|$)', '\\1' || $1 || '\\2')
+         WHERE regexp_replace(shopify_id,'\\D','','g') = $2`,
+        [r.truth, r.pid]);
+      console.log(`  ✓ ${r.vendor} ${r.pid}: ${r.live} -> ${r.truth}`);
+      ok++;
+    } catch (e) {
+      console.error(`  ✗ ${r.vendor} ${r.pid}: ${e.message}`);
+      fail++;
+    }
+  }
+  console.log(`\nDone. ok=${ok} fail=${fail}`);
+  await pool.end();
+})();

← 747a6821 auto-save: 2026-06-30T09:09:13 (5 files) — pending-approval/  ·  back to Designer Wallcoverings  ·  China Seas reconcile (DTD-A): source china_seas_catalog 106 4af735f6 →