[object Object]

← 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

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 →