← back to Filemaker Mcp
wallpaper: resolve private-label SKUs (mfr in metafields) + -Sample lines; add Content/Repeat/tags; DWPP vid=MDCKEN
f48d475d902db148c0aa7c33d2bab9fb6bbc621a · 2026-08-12 08:46:44 -0700 · Steve Abrams
Files touched
M lib/wallpaper.jsA scripts/dedup-dwpp-32837.mjs
Diff
commit f48d475d902db148c0aa7c33d2bab9fb6bbc621a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 12 08:46:44 2026 -0700
wallpaper: resolve private-label SKUs (mfr in metafields) + -Sample lines; add Content/Repeat/tags; DWPP vid=MDCKEN
---
lib/wallpaper.js | 64 +++++++++++++++++++++++++++++++++++++-------
scripts/dedup-dwpp-32837.mjs | 25 +++++++++++++++++
2 files changed, 80 insertions(+), 9 deletions(-)
diff --git a/lib/wallpaper.js b/lib/wallpaper.js
index 816939f..b74b924 100644
--- a/lib/wallpaper.js
+++ b/lib/wallpaper.js
@@ -38,9 +38,18 @@ const normalizeSku = (s) => String(s || '').toUpperCase().replace(/[^A-Z0-9]/g,
// a genuinely-new SKU may be CREATED.
const KNOWN_SERIES = new Set([
'CORK', 'DWGL', 'DWLG',
- 'DWC', 'DWSW', 'DWWC', 'DWLK', 'DWJS', 'DWBR', 'DWPH', 'DWAT', 'DWAF', 'DWKK',
+ 'DWC', 'DWSW', 'DWWC', 'DWLK', 'DWJS', 'DWBR', 'DWPH', 'DWPP', 'DWAT', 'DWAF', 'DWKK',
]);
+// Authoritative Series -> vid (vendor code) overrides for PRIVATE-LABEL series whose real
+// vendor is hidden behind a DW house brand in dw_unified (so vid can't be derived from the
+// source row) AND whose existing FileMaker masters carry a WRONG vid that the modal-vote
+// vidForSeries() would otherwise perpetuate. Checked BEFORE an existing master's own vid so
+// re-running corrects a previously-mis-stamped master. DWPP ("Phillipe Romano" London Paper
+// Weave / Prague Samarra etc.) is really MDC Wallcoverings — vid MDCKEN, NOT Wolf Gordon
+// ("wol"), which is what the 4 mislabeled DWPP masters had. (Steve, 2026-08-12.)
+const SERIES_VID = { DWPP: 'MDCKEN' };
+
// combo sku (DWKK134979, OP-ART-DECO-WAVES) -> {prefix, num, dashSku, key, confident}
// prefix = the leading DW series letters; num = the rest (pattern remainder, which may
// itself be all-alpha and hyphenated, e.g. "ART-DECO-WAVES"). key = the normalized
@@ -222,6 +231,13 @@ const SP_FIELDS = `json_build_object(
'width', COALESCE(metafields->'global'->'width'->>'value', metafields->'global'->>'width',
metafields->'dwc'->'width'->>'value', metafields->'dwc'->>'width',
metafields->'specs'->>'width'),
+ 'content', COALESCE(metafields->'dwc'->'contents'->>'value', metafields->'dwc'->>'contents',
+ metafields->'custom'->'material'->>'value', metafields->'custom'->>'material',
+ metafields->'global'->'Content'->>'value', metafields->'global'->>'Content'),
+ 'repeat', COALESCE(metafields->'specs'->'repeat'->>'value', metafields->'specs'->>'repeat',
+ metafields->'custom'->'pattern_repeat'->>'value', metafields->'custom'->>'pattern_repeat',
+ metafields->'custom'->'repeat'->>'value', metafields->'custom'->>'repeat',
+ metafields->'global'->'Repeat'->>'value', metafields->'global'->>'Repeat'),
'canonical', dw_sku)`;
// pull source fields for a dashed dw_sku from dw_unified (metafields-first).
@@ -243,8 +259,22 @@ function sourceFor(dashSku, key) {
ORDER BY (upper(regexp_replace(dw_sku,'[^A-Za-z0-9]','','g'))='${k}') DESC LIMIT 1`);
if (row && row.mfr) return { ...row, src: 'shopify_products(dw_sku normalized)' };
}
+ // Match on the LEGACY sku/variant_sku (with a trailing -Sample stripped) and read the
+ // mfr metafields-first via SP_FIELDS. Many private-label lines (e.g. Phillipe Romano
+ // "London Paper Weave" DWPP-2052xx) leave the mfr_sku AND dw_sku COLUMNS blank and file
+ // the real mfr number in metafields.custom.manufacturer_sku — and only on the -Sample
+ // row. So we must NOT gate on `mfr_sku<>''` (that skipped this whole class and returned
+ // "no mfr number"); instead let SP_FIELDS' metafields COALESCE supply the mfr and let the
+ // outer `if (row && row.mfr)` guard reject a genuinely blank result. When the plain and
+ // -Sample rows both match, prefer whichever actually carries a manufacturer number so the
+ // data-bearing (usually -Sample) row wins over a blank sellable row.
row = sql(`SELECT ${SP_FIELDS} FROM shopify_products
- WHERE upper(regexp_replace(COALESCE(NULLIF(sku,''),variant_sku),'-Sample$','','i')) = upper('${esc}') AND mfr_sku<>'' LIMIT 1`);
+ WHERE upper(regexp_replace(COALESCE(NULLIF(sku,''),variant_sku),'-Sample$','','i')) = upper('${esc}')
+ ORDER BY (COALESCE(NULLIF(mfr_sku,''),
+ metafields->'custom'->'manufacturer_sku'->>'value', metafields->'custom'->>'manufacturer_sku',
+ metafields->'global'->'mfr-pattern-number'->>'value', metafields->'global'->>'mfr-pattern-number',
+ metafields->'global'->'manufacturer_sku'->>'value', metafields->'global'->>'manufacturer_sku') IS NOT NULL) DESC
+ LIMIT 1`);
if (row && row.mfr) return { ...row, src: 'shopify_products(legacy sku)' };
// (3) universal vendor_catalog (all scraped vendors, keyed on dw_sku) — the authoritative
// source for private-label lines absent from shopify_products (e.g. DWBR/Malibu→Brewster).
@@ -344,7 +374,14 @@ function canonicalDashFor(key) {
// READ-ONLY resolve — what WOULD be written for a combo sku, with NO FileMaker writes.
// Returns { ok, mfr, vid, name, color, width, supplier, jpg, src } or { ok:false, reason }.
// Used by ensureWallpaper (below) and by dry-run/preview callers.
-export async function resolveWallpaperSource(combo) {
+export async function resolveWallpaperSource(comboRaw) {
+ // A memo-sample order line arrives as the "-Sample" variant (e.g. DWPP-205219-Sample),
+ // but the WALLPAPER master is per PATTERN, not per sample-vs-sellable — the sample and
+ // the roll share one master. Strip a trailing -Sample so the sample line resolves to the
+ // same master (and so the number-tail / component-split logic sees the real SKU, not one
+ // ending in the word "Sample"). Consistent with the '-Sample$' stripping already done
+ // inside every sourceFor() query.
+ const combo = String(comboRaw || '').replace(/[-_ ]?sample$/i, '').trim();
const p = parseCombo(combo);
if (!p) return { ok: false, flagged: combo, reason: 'unparseable sku' };
const s = sourceFor(p.dashSku, p.key);
@@ -366,10 +403,14 @@ export async function resolveWallpaperSource(combo) {
note: `Series|JS-Pattern split for "${combo}" is not derivable from the string and no existing master matched; not creating.` };
}
if (!s || !s.mfr) return { ok: false, flagged: combo, reason: `no mfr number (${s ? s.src : 'not in dw_unified'})`, _p: p, _existing: ex };
- const vid = ex.vid || await vidForSeries(p.prefix);
+ // vid precedence: (1) an authoritative Series override (fixes private-label series whose
+ // existing masters are mis-stamped), then (2) the SKU's own existing-master vid, then
+ // (3) the series modal-vote guess for a genuinely-new series.
+ const vid = SERIES_VID[p.prefix] || ex.vid || await vidForSeries(p.prefix);
const name = s.pattern || '', color = s.color || '', width = widthClean(s.width);
+ const content = s.content || '', repeat = s.repeat || '';
const jpg = [name, color].filter(Boolean).join(' - ');
- return { ok: true, mfr: s.mfr, vid, name, color, width, supplier: s.supplier || '', jpg, src: s.src, _p: p, _existing: ex };
+ return { ok: true, mfr: s.mfr, vid, name, color, width, content, repeat, supplier: s.supplier || '', jpg, src: s.src, _p: p, _existing: ex };
}
// Ensure/complete the WALLPAPER record for a combo sku (DW SKU, no dashes, no -Sample).
@@ -379,6 +420,7 @@ export async function ensureWallpaper(combo) {
if (!r.ok) return r;
const p = r._p, s = { mfr: r.mfr, supplier: r.supplier };
const vid = r.vid, name = r.name, color = r.color, width = r.width, jpg = r.jpg;
+ const content = r.content, repeat = r.repeat;
// Reuse the alias-/error-aware lookup already done in resolveWallpaperSource.
// NEVER create when existence could not be verified (auth blip / timeout / lock):
// treating a real error as "not found" is exactly what minted duplicate masters.
@@ -399,16 +441,20 @@ export async function ensureWallpaper(combo) {
if (!String(p.prefix || '').trim()) {
return { ok: false, flagged: combo, reason: 'blank-series — refusing to create a master with an empty Series' };
}
- const res = await fm.createRecord('WALLPAPER', ENTRY, { 'Mfr Pattern': s.mfr, 'JS Pattern': p.num, Series: p.prefix, Supplier: s.supplier || '', Width: width }, { dryRun: false }).catch((e) => ({ err: e.fmCode }));
+ const res = await fm.createRecord('WALLPAPER', ENTRY, { 'Mfr Pattern': s.mfr, 'JS Pattern': p.num, Series: p.prefix, Supplier: s.supplier || '', Width: width, Repeat: repeat || '' }, { dryRun: false }).catch((e) => ({ err: e.fmCode }));
if (res.err) return { ok: false, flagged: combo, reason: `create failed ${res.err}` };
id = res.recordId;
} else {
- for (const [k, v] of [['Mfr Pattern', s.mfr], ['Supplier', s.supplier || ''], ['Width', width]]) { if (v) { try { await fm.updateRecord('WALLPAPER', ENTRY, id, { [k]: v }, { dryRun: false }); } catch {} } }
+ for (const [k, v] of [['Mfr Pattern', s.mfr], ['Supplier', s.supplier || ''], ['Width', width], ['Repeat', repeat]]) { if (v) { try { await fm.updateRecord('WALLPAPER', ENTRY, id, { [k]: v }, { dryRun: false }); } catch {} } }
}
- for (const [k, v] of [['Name of Pattern', name], ['Color of Pattern', color], ['vid', vid], ['JPG Name', jpg]]) {
+ // Full-View-only descriptive fields: Name/Color/vid/JPG plus Content (the material/
+ // contents field) and MetDataSearchWord (the searchable "tags" field) — so the master
+ // (and the invoice's live lookups) carry the full spec and the real mfr number is
+ // findable by tag/search. (Steve: "put the real mfr number in the tags too", 2026-08-12.)
+ for (const [k, v] of [['Name of Pattern', name], ['Color of Pattern', color], ['vid', vid], ['JPG Name', jpg], ['Content', content], ['Repeat', repeat], ['MetDataSearchWord', s.mfr]]) {
if (v) { try { await fm.updateRecord('WALLPAPER', FULL, id, { [k]: v }, { dryRun: false }); } catch {} }
}
- return { ok: true, mfr: s.mfr, vid, name, color, width, supplier: s.supplier || '', src: r.src };
+ return { ok: true, mfr: s.mfr, vid, name, color, width, content, repeat, supplier: s.supplier || '', src: r.src };
}
// Internal helpers exported for the TK-10083 live-proof dry-run so it exercises the ACTUAL
diff --git a/scripts/dedup-dwpp-32837.mjs b/scripts/dedup-dwpp-32837.mjs
new file mode 100644
index 0000000..73c5a73
--- /dev/null
+++ b/scripts/dedup-dwpp-32837.mjs
@@ -0,0 +1,25 @@
+// Dedup the two DWPP masters duplicated during the 32837 order fix (FileMaker Cloud
+// create-then-find index latency minted a 2nd master on the corrective re-run). For each,
+// KEEP the fully-corrected master (vid MDCKEN + mfr in tags + Content) and archive+delete
+// the blank straggler. Archive-first (restore JSON) per the sanctioned dedup pattern.
+// node scripts/dedup-dwpp-32837.mjs # archive + delete
+// node scripts/dedup-dwpp-32837.mjs --dry-run # show only
+import { readFileSync, writeFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path';
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+for (const l of readFileSync(join(ROOT,'.env'),'utf8').split('\n')) { const m=l.match(/^([A-Z0-9_]+)=(.*)$/); if(m&&!process.env[m[1]])process.env[m[1]]=m[2].replace(/^['"]|['"]$/g,''); }
+const DRY = process.argv.includes('--dry-run');
+const { getRecord, deleteRecord } = await import('../src/fm-client.js');
+const DB='WALLPAPER', L='Basic List of Fields', FULL='*List Wallpapers - Full View';
+const DELETE=[{ sku:'DWPP-206897', keep:'539328', drop:'539333' },{ sku:'DWPP-205219', keep:'539329', drop:'539334' }];
+for (const d of DELETE) {
+ const rec = await getRecord(DB, FULL, d.drop).catch(e=>({err:e.message}));
+ if (rec.err) { console.log(`${d.sku}: drop ${d.drop} not fetchable (${rec.err}) — skip`); continue; }
+ const arch = join(ROOT,'data',`deleted-record-${d.drop}-restore.json`);
+ writeFileSync(arch, JSON.stringify({ note:`DWPP dedup 32837 fix 2026-08-12; keep ${d.keep}`, db:DB, recordId:d.drop, record:rec }, null, 2));
+ console.log(`${d.sku}: ARCHIVED drop ${d.drop} -> ${arch}`);
+ if (DRY) { console.log(` [dry-run] would delete ${d.drop}, keep ${d.keep}`); continue; }
+ await deleteRecord(DB, L, d.drop);
+ try { await getRecord(DB, L, d.drop); console.log(` WARN: ${d.drop} still fetchable`); }
+ catch { console.log(` DELETED ${d.drop}; kept ${d.keep}`); }
+}
← ef0b961 Add GRS/WQ 20% price-bump script + canonical grasscloth impo
·
back to Filemaker Mcp
·
add dedup-dwpp-32837 script + 539333/539334 restore archives dcd8c12 →