← 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
A scripts/bump-grs-wq-price.mjsA scripts/create-grasscloth-masters.mjs
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 →