[object Object]

← back to Filemaker Mcp

Fix client double-entry + Company/Name field swap in Shopify→FileMaker sync

de6eb8178daa2da1815d38b6233038500601a17f · 2026-07-16 09:24:31 -0700 · Steve Abrams

- Company field now holds the business name, Name holds the contact person
  (was reversed: put e.g. 'Denny Wilson' in Company, 'Wilson Construction' in Name)
- Dedup rewritten as priority waterfall: email → company → client name,
  searching the correct fields (old code searched Company against the person's
  name, silently missing real dupes and creating second records)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit de6eb8178daa2da1815d38b6233038500601a17f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 16 09:24:31 2026 -0700

    Fix client double-entry + Company/Name field swap in Shopify→FileMaker sync
    
    - Company field now holds the business name, Name holds the contact person
      (was reversed: put e.g. 'Denny Wilson' in Company, 'Wilson Construction' in Name)
    - Dedup rewritten as priority waterfall: email → company → client name,
      searching the correct fields (old code searched Company against the person's
      name, silently missing real dupes and creating second records)
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 lib/sync-core.js | 74 ++++++++++++++++++++++++++++++++++++--------------------
 1 file changed, 48 insertions(+), 26 deletions(-)

diff --git a/lib/sync-core.js b/lib/sync-core.js
index 09ebf75..95ff20a 100644
--- a/lib/sync-core.js
+++ b/lib/sync-core.js
@@ -1,12 +1,11 @@
 // Shopify order -> FileMaker Clients sync core.
 // Maps a Shopify order's customer to a FileMaker Clients record, but only
-// creates one when NO similar client already exists (deduped by email, phone,
-// OR name — per Steve's rule "always check for similar or same name based on
-// name, email, or phone number").
+// creates one when NO similar client already exists. Dedup is checked in strict
+// priority order — EMAIL first, then COMPANY name, then CLIENT (contact) name —
+// per Steve's rule (2026-07-16). Company = business name, Name = contact person.
 import * as fm from '../src/fm-client.js';
 
 const LAYOUT = 'wALLPAPER oPTIONS'; // Clients layout exposing the client header fields
-const norm = (s) => (s || '').toString().trim().toLowerCase().replace(/\s+/g, ' ');
 const digits = (s) => (s || '').toString().replace(/\D/g, '');
 const last10 = (s) => digits(s).slice(-10);
 
@@ -18,8 +17,14 @@ export function mapOrder(o) {
     orderNumber: o.order_number,
     orderId: o.id,
     fields: {
-      'Company': fullName || s.company || '',
-      'Name': s.company || '',
+      // FileMaker CLIENT record: Company = the BUSINESS name, Name = the CONTACT
+      // person. Previously these were reversed (fullName went into Company,
+      // company into Name), which put e.g. "Denny Wilson" in Company and
+      // "Wilson Construction" in Name. Fixed 2026-07-16 (Steve flagged).
+      // Fall back to the person's name for Company only when there is no
+      // business name (individual buyer), preserving the old behavior there.
+      'Company': s.company || fullName || '',
+      'Name': fullName || '',
       'Address': s.address1 || '',
       'City': s.city || '',
       'State': s.province_code || '',
@@ -31,31 +36,48 @@ export function mapOrder(o) {
 }
 
 // Returns an array of matching existing clients (empty = safe to create).
+//
+// Steve's dedup rule (2026-07-16): ALWAYS check in priority order —
+//   1) Email address   2) Company name   3) Client (contact) name
+// We check tier by tier and return the FIRST tier that produces a match, so a
+// client already on file is never entered twice. Phone rides with the email
+// tier as an equally-strong identity signal. Matching the CORRECT fields is
+// essential: the old code searched Company against the person's name (because
+// the field mapping was reversed), which silently missed real dupes and let
+// second records get created.
 export async function findDuplicates({ fields }) {
-  const email = norm(fields['Email address']);
+  const email = fields['Email address'];
   const phone10 = last10(fields['Phone']);
-  const name = norm(fields['Company']);
-  const queries = [];
-  if (email) queries.push({ 'Email address': '==' + fields['Email address'] });
-  if (phone10) queries.push({ 'Phone': '*' + phone10 + '*' });          // contains last-10 digits
-  if (name) queries.push({ 'Company': '==' + fields['Company'] });        // exact display name
-  if (name) queries.push({ 'Name': '==' + fields['Company'] });
+  const company = fields['Company']; // business name
+  const person = fields['Name'];     // contact person
 
-  const hits = new Map(); // recordId -> {acct, company, why[]}
-  for (const q of queries) {
-    let recs = [];
-    try { recs = (await fm.findRecords('Clients', LAYOUT, q, { limit: 10 })).records; }
-    catch (e) { if (e.fmCode === '401') continue; /* 401 = no records found */ else continue; }
-    const why = Object.keys(q)[0];
-    for (const r of recs) {
-      // confirm phone match by normalized last-10 to avoid wildcard false positives
-      if (why === 'Phone' && last10(r.fieldData['Phone']) !== phone10) continue;
-      const e = hits.get(r.recordId) || { recordId: r.recordId, acct: r.fieldData['Account Number'], company: r.fieldData['Company'], why: [] };
-      if (!e.why.includes(why)) e.why.push(why);
-      hits.set(r.recordId, e);
+  const tiers = [
+    { queries: [
+        ...(email ? [{ 'Email address': '==' + email }] : []),
+        ...(phone10 ? [{ 'Phone': '*' + phone10 + '*' }] : []), // contains last-10
+      ] },
+    { queries: company ? [{ 'Company': '==' + company }] : [] }, // exact company
+    { queries: person ? [{ 'Name': '==' + person }] : [] },      // exact contact name
+  ];
+
+  for (const tier of tiers) {
+    const hits = new Map(); // recordId -> {acct, company, why[]}
+    for (const q of tier.queries) {
+      let recs = [];
+      try { recs = (await fm.findRecords('Clients', LAYOUT, q, { limit: 10 })).records; }
+      catch (e) { continue; /* 401 = no records found */ }
+      const why = Object.keys(q)[0];
+      for (const r of recs) {
+        // confirm phone match by normalized last-10 to avoid wildcard false positives
+        if (why === 'Phone' && last10(r.fieldData['Phone']) !== phone10) continue;
+        const e = hits.get(r.recordId) || { recordId: r.recordId, acct: r.fieldData['Account Number'], company: r.fieldData['Company'], why: [] };
+        if (!e.why.includes(why)) e.why.push(why);
+        hits.set(r.recordId, e);
+      }
     }
+    if (hits.size) return [...hits.values()]; // first tier with a match wins
   }
-  return [...hits.values()];
+  return [];
 }
 
 // Process one order end-to-end: ensure the client exists (create if new, dedup

← 65eb72c fix: idempotency guard — skip invoice creation when FileMake  ·  back to Filemaker Mcp  ·  Harden client dedup + add read-only single-order test 96da496 →