[object Object]

← back to Filemaker Mcp

Add GRS/WQ 20% price-bump script + canonical grasscloth importer (name/color mapped, writes via Full View layout)

ef0b961110f5b0e7b732b0858563cb32d725e499 · 2026-08-10 11:11:54 -0700 · Steve

Files touched

Diff

commit ef0b961110f5b0e7b732b0858563cb32d725e499
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Aug 10 11:11:54 2026 -0700

    Add GRS/WQ 20% price-bump script + canonical grasscloth importer (name/color mapped, writes via Full View layout)
---
 scripts/bump-grs-wq-price.mjs         | 41 ++++++++++++++++++++
 scripts/create-grasscloth-masters.mjs | 72 +++++++++++++++++++++++++++++++++++
 2 files changed, 113 insertions(+)

diff --git a/scripts/bump-grs-wq-price.mjs b/scripts/bump-grs-wq-price.mjs
new file mode 100644
index 0000000..8b1194a
--- /dev/null
+++ b/scripts/bump-grs-wq-price.mjs
@@ -0,0 +1,41 @@
+// Raise Retail Price by 20% on the recent WallQuest specialty-grasscloth WALLPAPER
+// masters (Series=GRS, vid=WQ, JS Pattern 300001..300078 — today's batch only, NOT the
+// legacy GRS backlog). new = round(current * 1.20, 2). Skips records with no numeric price.
+// Idempotent guard: writes a marker? No — price bumps are NOT self-idempotent, so this is
+// deliberately scoped + dry-run-by-default. Run it ONCE with --apply.
+//
+//   node bump-grs-wq-price.mjs            # DRY-RUN (prints old -> new)
+//   node bump-grs-wq-price.mjs --apply    # commit
+import * as fm from '../src/fm-client.js';
+
+const APPLY = process.argv.includes('--apply');
+const FACTOR = 1.20;
+const DB = 'WALLPAPER';
+const LAYOUT = '*List Wallpapers - Full View';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const round2 = n => Math.round((n + Number.EPSILON) * 100) / 100;
+
+async function main() {
+  const res = await fm.findRecords(DB, LAYOUT,
+    { Series: 'GRS', vid: 'WQ', 'JS Pattern': '300001...300078' },
+    { limit: 500 });
+  const recs = res.records || [];
+  console.log(`Batch: ${recs.length} GRS/WQ records (JS 300001..300078) · +${Math.round((FACTOR-1)*100)}% · ${APPLY ? 'APPLY' : 'DRY-RUN'}\n`);
+
+  let ok = 0, skip = 0, fail = 0;
+  for (const rec of recs) {
+    const fd = rec.fieldData;
+    const sku = fd['combo sku'];
+    const cur = parseFloat(fd['Retail Price']);
+    if (!isFinite(cur) || cur <= 0) { skip++; console.log(`  SKIP ${sku} (no price: "${fd['Retail Price']}")`); continue; }
+    const next = round2(cur * FACTOR);
+    if (!APPLY) { if (ok < 80) console.log(`  ${sku}  ${cur}  ->  ${next}`); ok++; continue; }
+    try {
+      await fm.updateRecord(DB, LAYOUT, rec.recordId, { 'Retail Price': next }, { dryRun: false });
+      ok++; if (ok % 20 === 0) console.log(`  ...updated ${ok}`);
+      await sleep(120);
+    } catch (e) { fail++; console.error(`  FAIL ${sku}: ${e.fmCode || e.message}`); await sleep(400); }
+  }
+  console.log(`\n${APPLY ? 'Updated' : 'Planned'}=${ok}  skippedNoPrice=${skip}  fail=${fail}`);
+}
+main().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/create-grasscloth-masters.mjs b/scripts/create-grasscloth-masters.mjs
new file mode 100644
index 0000000..22c331b
--- /dev/null
+++ b/scripts/create-grasscloth-masters.mjs
@@ -0,0 +1,72 @@
+// CANONICAL importer: create FileMaker WALLPAPER masters for WallQuest specialty
+// grasscloths from dw_unified.specialty_grasscloths_stage — with EVERY field mapped,
+// including "Name of Pattern" and "Color of Pattern" (the two the previous ad-hoc run
+// dropped, leaving 77 records blank on 2026-08-10; see backfill-grs-name-color.mjs).
+//
+// ROOT-CAUSE FIXES baked in here so the next batch is correct at insert time:
+//   1) Field mapping — stage.pattern -> "Name of Pattern", stage.color -> "Color of Pattern".
+//   2) WRITE LAYOUT — uses "*List Wallpapers - Full View" (which EXPOSES Name/Color of
+//      Pattern). The old path wrote through "Add wallcovering", a layout that lacks those
+//      two fields, so they silently never persisted. Never create these through a layout
+//      that doesn't expose every field you're writing.
+//   3) Idempotent by Mfr Pattern (vid=WQ) — re-running never dupes.
+//
+// Series/JS Pattern derive from the canonical stage.dw_sku (e.g. GRS-801346 -> Series GRS,
+// JS 801346). NOTE: the 2026-08-10 ad-hoc batch used a different GRS3000xx sequence; confirm
+// the numbering scheme with Steve before a real --apply run if that sequence must continue.
+//
+//   node create-grasscloth-masters.mjs                 # DRY-RUN
+//   node create-grasscloth-masters.mjs --apply [--limit=N] [--offset=N]
+import { execFileSync } from 'node:child_process';
+import * as fm from '../src/fm-client.js';
+
+const APPLY = process.argv.includes('--apply');
+const LIMIT = parseInt((process.argv.find(a => a.startsWith('--limit=')) || '').split('=')[1] || '0', 10);
+const OFFSET = parseInt((process.argv.find(a => a.startsWith('--offset=')) || '').split('=')[1] || '0', 10);
+const DB = 'WALLPAPER';
+const LAYOUT = '*List Wallpapers - Full View';   // exposes Name/Color of Pattern — REQUIRED
+const PSQL = process.env.PSQL_BIN || '/opt/homebrew/opt/postgresql@14/bin/psql';
+const PG = 'postgresql:///dw_unified?host=/tmp&user=stevestudio2';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+// "36 in | 91.4 cm" -> '36" Wide (trim to 34")'  (grasscloth convention: trim 2" off)
+const widthOf = w => { const m = String(w || '').match(/([\d.]+)\s*in/i) || String(w || '').match(/([\d.]+)/);
+  if (!m) return ''; const inch = Math.round(parseFloat(m[1])); return `${inch}" Wide (trim to ${inch - 2}")`; };
+
+const rows = JSON.parse(execFileSync(PSQL, [PG, '-tAc',
+  `select coalesce(json_agg(json_build_object(
+      'mfr',lower(mfr_sku),'name',name,'pattern',pattern,'color',color,
+      'width',width,'price',our_price,'dw_sku',dw_sku) order by dw_sku)::text,'[]')
+    from specialty_grasscloths_stage where coalesce(status,'active')='active'`],
+  { encoding: 'utf8', maxBuffer: 128e6 }).trim());
+
+async function exists(mfr) {
+  const r = await fm.findRecords(DB, LAYOUT, { 'Mfr Pattern': '==' + mfr, vid: '==WQ' }, { limit: 1 })
+    .catch(() => ({ records: [] }));
+  return (r.records?.length || 0) > 0;
+}
+
+async function main() {
+  const todo = rows.slice(OFFSET, LIMIT ? OFFSET + LIMIT : undefined);
+  console.log(`specialty_grasscloths_stage(active): ${rows.length} · this run: ${todo.length} · ${APPLY ? 'APPLY' : 'DRY-RUN'}\n`);
+  let ok = 0, skip = 0, fail = 0;
+  for (const r of todo) {
+    const [prefix, num] = String(r.dw_sku || '').split('-');
+    if (!prefix || !num) { skip++; console.log(`  SKIP ${r.mfr} (no dw_sku)`); continue; }
+    const fd = {
+      Series: prefix, 'JS Pattern': num, 'Mfr Pattern': r.mfr,
+      'Name of Pattern': r.pattern, 'Color of Pattern': r.color || '',
+      Width: widthOf(r.width), 'Retail Price': r.price ?? '',
+      'Internal Description': r.name, vid: 'WQ',
+    };
+    if (!APPLY) { if (ok < 8) console.log(`  ${r.dw_sku}  ${r.mfr}  "${fd['Name of Pattern']}" (${fd['Color of Pattern'] || '—'})  w=${fd.Width}  $${fd['Retail Price']}`); ok++; continue; }
+    try {
+      if (await exists(r.mfr)) { skip++; continue; }
+      await fm.createRecord(DB, LAYOUT, fd, { dryRun: false });
+      ok++; if (ok % 20 === 0) console.log(`  ...created ${ok}`);
+      await sleep(150);
+    } catch (e) { fail++; console.error(`  FAIL ${r.mfr}: ${e.fmCode || e.message}`); await sleep(400); }
+  }
+  console.log(`\n${APPLY ? 'created' : 'planned'}=${ok}  skipped=${skip}  fail=${fail}`);
+}
+main().catch(e => { console.error(e); process.exit(1); });

← dd37d30 Backfill blank Name/Color of Pattern on WallQuest GRS grassc  ·  back to Filemaker Mcp  ·  wallpaper: resolve private-label SKUs (mfr in metafields) + f48d475 →