[object Object]

← back to Sample Followup Sweep

sample-followup: add vendor Ref/Tracking + Pattern-Name columns; add LBI Boyd contact

70ec59696741c5761e4de01b2b078a85892cc94b · 2026-09-01 11:52:17 -0700 · Steve

Steve 9/01: (1) when a vendor REPLIES about an item with their own order/confirmation #
or a shipment tracking #, capture it (conservatively — only when their inbound mail names
THAT sku) and show it as a 'Your Ref / Tracking' column. (2) when a mfr SKU is blank, show
the pattern NAME in its own column. Both columns render only when populated, so normal
letters stay two-column. Added LBI Boyd sample contact (jill@lbiboyd.com, found in our
email) so the KOT 23 item drafts instead of skipping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 70ec59696741c5761e4de01b2b078a85892cc94b
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Sep 1 11:52:17 2026 -0700

    sample-followup: add vendor Ref/Tracking + Pattern-Name columns; add LBI Boyd contact
    
    Steve 9/01: (1) when a vendor REPLIES about an item with their own order/confirmation #
    or a shipment tracking #, capture it (conservatively — only when their inbound mail names
    THAT sku) and show it as a 'Your Ref / Tracking' column. (2) when a mfr SKU is blank, show
    the pattern NAME in its own column. Both columns render only when populated, so normal
    letters stay two-column. Added LBI Boyd sample contact (jill@lbiboyd.com, found in our
    email) so the KOT 23 item drafts instead of skipping.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 data/contacts.json        | 53 ++++++++++++++++++++++++++---------------------
 lib/compose.js            | 22 ++++++++++----------
 scripts/scheduled-run.mjs | 40 +++++++++++++++++++++++++++++++++--
 3 files changed, 78 insertions(+), 37 deletions(-)

diff --git a/data/contacts.json b/data/contacts.json
index 32ede54..4bbfc64 100644
--- a/data/contacts.json
+++ b/data/contacts.json
@@ -107,17 +107,17 @@
     "account_number": "On File"
   },
   "san": {
-    "name": "Sancar (AS Cr\u00e9ation) \u2014 inferred",
+    "name": "Sancar (AS Création) — inferred",
     "sample_email": "sales@sancarwallcovering.com",
     "account_number": "1674444"
   },
   "chin": {
-    "name": "China Seas by Quadrille \u2014 inferred",
+    "name": "China Seas by Quadrille — inferred",
     "sample_email": "Rhaiza@quadrilleinc.com",
     "account_number": "37017"
   },
   "jofa": {
-    "name": "Lee Jofa (Kravet family) \u2014 inferred",
+    "name": "Lee Jofa (Kravet family) — inferred",
     "sample_email": "matt.schoffman@kravet.com",
     "account_number": "10087117"
   },
@@ -142,20 +142,20 @@
     "account_number": "On File"
   },
   "spn": {
-    "name": "Spoonflower (online \u2014 no rep desk)",
+    "name": "Spoonflower (online — no rep desk)",
     "disposition": "no-chase",
-    "disposition_reason": "online print-on-demand \u2014 no sample-order desk to email (vendor type has no rep)",
+    "disposition_reason": "online print-on-demand — no sample-order desk to email (vendor type has no rep)",
     "disposition_set": "2026-08-15"
   },
   "pf": {
     "name": "Pierre Frey",
     "sample_email": "mocampo@kneedlerfauchere.com",
     "account_number": "On File",
-    "contact_source": "DW Sent history \u2014 Kneedler Fauch\u00e8re (PF LA rep), order correspondence 2026-08-12; CONFIRM before send"
+    "contact_source": "DW Sent history — Kneedler Fauchère (PF LA rep), order correspondence 2026-08-12; CONFIRM before send"
   },
   "rad": {
     "disposition": "no-chase",
-    "disposition_reason": "item Dig-4350114V shares Spoonflower DIG- digital-print prefix \u2014 POD/no sample desk (INFERRED; reverse if RAD is a real chaseable vendor)",
+    "disposition_reason": "item Dig-4350114V shares Spoonflower DIG- digital-print prefix — POD/no sample desk (INFERRED; reverse if RAD is a real chaseable vendor)",
     "disposition_set": "2026-08-15"
   },
   "wol": {
@@ -184,10 +184,10 @@
     "sample_email": "ddawson@designtex.com",
     "account_number": "On File",
     "needs_confirm": true,
-    "contact_source": "George: Donna Dawson, Representative (LA), price-check corr 8/6/2026 \u2014 rep not sample desk, CONFIRM"
+    "contact_source": "George: Donna Dawson, Representative (LA), price-check corr 8/6/2026 — rep not sample desk, CONFIRM"
   },
   "mom": {
-    "name": "Momentum (\u2192Hollywood, INTERNAL chase only)",
+    "name": "Momentum (→Hollywood, INTERNAL chase only)",
     "sample_email": "customerservice@momtex.com",
     "account_number": "On File",
     "needs_confirm": true,
@@ -198,12 +198,12 @@
     "sample_email": "sales@sancarwallcovering.com",
     "account_number": "1674444",
     "needs_confirm": true,
-    "contact_source": "George: 'sancar (VERSACE)' memo 8/17/2026 \u2014 Versace ordered through Sancar desk"
+    "contact_source": "George: 'sancar (VERSACE)' memo 8/17/2026 — Versace ordered through Sancar desk"
   },
   "sch": {
     "name": "Schumacher (F. Schumacher)",
     "disposition": "no-chase",
-    "disposition_reason": "self-service online sample ordering; only noreply@mailer.fschumacher.com exists \u2014 no rep desk to chase (George-verified 8/20)",
+    "disposition_reason": "self-service online sample ordering; only noreply@mailer.fschumacher.com exists — no rep desk to chase (George-verified 8/20)",
     "disposition_set": "2026-08-20"
   },
   "sir": {
@@ -211,7 +211,7 @@
     "sample_email": "sales@sir-edward.com",
     "account_number": "On File",
     "needs_confirm": true,
-    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): D\u00e9sir\u00e9e Ouweneel <sales@sir-edward.com>, active sample/sales corr; DWFM40xxx file-requested art items"
+    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): Désirée Ouweneel <sales@sir-edward.com>, active sample/sales corr; DWFM40xxx file-requested art items"
   },
   "cow": {
     "name": "Cowtan & Tout / Colefax & Fowler / Jane Churchill",
@@ -242,11 +242,11 @@
     "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): Therese Stachowiak <therese@egg-and-dart.com>, same desk as arte; BEC SKU"
   },
   "coor": {
-    "name": "Coordonn\u00e9 (via NewWall)",
+    "name": "Coordonné (via NewWall)",
     "sample_email": "tricia@newwall.com",
     "account_number": "On File",
     "needs_confirm": true,
-    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): Tricia Robb <tricia@newwall.com> handles NWD/Coordonn\u00e9; DWDC 'Bike ride Fawn' A00907"
+    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): Tricia Robb <tricia@newwall.com> handles NWD/Coordonné; DWDC 'Bike ride Fawn' A00907"
   },
   "christian-lacroix-europe": {
     "name": "Christian Lacroix (via Osborne & Little / Designers Guild)",
@@ -260,31 +260,31 @@
     "sample_email": "support@hyggeandwest.com",
     "account_number": "On File",
     "needs_confirm": true,
-    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): only Hygge & West address on file is support@hyggeandwest.com (marketing/support); DWHW2 'Be Good' SKU \u2014 CONFIRM it accepts trade sample requests"
+    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): only Hygge & West address on file is support@hyggeandwest.com (marketing/support); DWHW2 'Be Good' SKU — CONFIRM it accepts trade sample requests"
   },
   "fol": {
     "name": "Folia Fabrics (RETIRED)",
     "disposition": "no-chase",
-    "disposition_reason": "Folia source decommissioned 2026-08-18 (folia-scraper retired); Ralph LWP items were Folia-distributed \u2014 no live desk to chase",
+    "disposition_reason": "Folia source decommissioned 2026-08-18 (folia-scraper retired); Ralph LWP items were Folia-distributed — no live desk to chase",
     "disposition_set": "2026-08-20",
     "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns)"
   },
   "phillipe-romano": {
     "name": "Phillipe Romano (private label)",
     "disposition": "no-chase",
-    "disposition_reason": "DW private label (Command54/RIGO/Justin David umbrella) \u2014 internal only, never chase a customer-facing vendor desk; PI-ACOUSTIC DWPN SKU",
+    "disposition_reason": "DW private label (Command54/RIGO/Justin David umbrella) — internal only, never chase a customer-facing vendor desk; PI-ACOUSTIC DWPN SKU",
     "disposition_set": "2026-08-20",
     "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns)"
   },
   "carl": {
     "name": "Carlsten / Desima (private label, archived)",
     "disposition": "no-chase",
-    "disposition_reason": "Carlsten/Desima private-label line (Steve: Desima/Kravet); TTF Capitone items \u2014 archived line, no live sample desk",
+    "disposition_reason": "Carlsten/Desima private-label line (Steve: Desima/Kravet); TTF Capitone items — archived line, no live sample desk",
     "disposition_set": "2026-08-20",
     "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns)"
   },
   "ftr": {
-    "name": "(unresolved) metallic-leaf vendor \u2014 GLM/GLMG SL08/B20 gunmetal/bronze/copper codes",
+    "name": "(unresolved) metallic-leaf vendor — GLM/GLMG SL08/B20 gunmetal/bronze/copper codes",
     "needs_confirm": true,
     "unresolved_email": true,
     "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): no vendor identity found in George; metallic/gilded-leaf line, distributor unknown"
@@ -293,19 +293,19 @@
     "name": "Ulrich Welter",
     "needs_confirm": true,
     "unresolved_email": true,
-    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): George: our sample requests addressed 'Attn: Sample Dept Ulrich Welter' (J-codes, DWS prefix) \u2014 vendor NAME confirmed but no reply-email captured; needs an Ulrich Welter sample-desk address before it can draft"
+    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): George: our sample requests addressed 'Attn: Sample Dept Ulrich Welter' (J-codes, DWS prefix) — vendor NAME confirmed but no reply-email captured; needs an Ulrich Welter sample-desk address before it can draft"
   },
   "1838-wallcoverings": {
     "name": "1838 Wallcoverings",
     "needs_confirm": true,
     "unresolved_email": true,
-    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): real line (1838-scraper-manager); likely distributed via Maxwell Fabrics (distribution@postmark.pdfsystems.com is order-only, not a sample desk) \u2014 no inbound sample-desk contact captured; CONFIRM the 1838/Maxwell sample email"
+    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): real line (1838-scraper-manager); likely distributed via Maxwell Fabrics (distribution@postmark.pdfsystems.com is order-only, not a sample desk) — no inbound sample-desk contact captured; CONFIRM the 1838/Maxwell sample email"
   },
   "poi": {
     "name": "Pointe (via Justin David private label)",
     "needs_confirm": true,
     "unresolved_email": true,
-    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): PI- codes DWPR/DWPN, 'from pointe'; Justin David desk jackie@justindavid.com is the likely source but POI may be an internal PR/JD private-label \u2014 CONFIRM whether to chase externally at all"
+    "contact_source": "George/Gmail mining 2026-08-20 (grounded on live outstanding-memo Mfr Patterns): PI- codes DWPR/DWPN, 'from pointe'; Justin David desk jackie@justindavid.com is the likely source but POI may be an internal PR/JD private-label — CONFIRM whether to chase externally at all"
   },
   "paul": {
     "name": "Paul Montgomery",
@@ -321,7 +321,7 @@
     "contact_source": "George/Gmail mining 2026-08-25 (grounded on live outstanding-memo Mfr Patterns + account#); MDC Customer Service <cs@mdcwall.com>"
   },
   "eli": {
-    "name": "Elitis (USA \u2014 Annalisa Dominguez)",
+    "name": "Elitis (USA — Annalisa Dominguez)",
     "main_email": "ANNALISA@elitis.us",
     "account_number": "1062120",
     "needs_confirm": true,
@@ -332,6 +332,11 @@
     "main_email": "rebecca.cragg@lincrusta.com",
     "account_number": "1062465",
     "needs_confirm": true,
-    "contact_source": "George/Gmail mining 2026-08-25 (grounded on live outstanding-memo Mfr Patterns + account#); active rep Rebecca Cragg (corr. through 8/18/2026) \u2014 main/rep fallback per Steve 8/25"
+    "contact_source": "George/Gmail mining 2026-08-25 (grounded on live outstanding-memo Mfr Patterns + account#); active rep Rebecca Cragg (corr. through 8/18/2026) — main/rep fallback per Steve 8/25"
+  },
+  "lbi": {
+    "name": "LBI Boyd (Jill Carol)",
+    "sample_email": "jill@lbiboyd.com",
+    "account_number": "On File"
   }
 }
\ No newline at end of file
diff --git a/lib/compose.js b/lib/compose.js
index 9f9ca2a..98f7028 100644
--- a/lib/compose.js
+++ b/lib/compose.js
@@ -10,13 +10,16 @@ function compose(vendor, rows) {
   const account = vendor.account_number || '‹CONFIRM DW ACCOUNT #›';
   // Steve 8/20: fall back to the vendor's main email when no sample email exists.
   const to = vendor.sample_email || vendor.main_email || '‹CONFIRM SAMPLE EMAIL›';
-  // Steve 9/01: show ONLY the DATE WE ORDERED + the MANUFACTURER # (the vendor's own SKU).
-  // Do NOT show our internal DW reference number ("combo sku" / DW###) — it's meaningless to the
-  // supplier and must never be presented as the item's SKU.
-  const rowsHtml = rows.map(r => `<tr style="border-bottom:1px solid #eee">
-<td style="padding:5px 18px 5px 0;white-space:nowrap">${esc(r.requested || '—')}</td>
-<td style="padding:5px 0"><strong>${esc(r.mfr || '—')}</strong></td>
-</tr>`).join('\n');
+  // Steve 9/01: show the DATE WE ORDERED + the MANUFACTURER # (the vendor's own SKU). NEVER our
+  // internal DW reference ("combo sku" / DW###). Two conditional columns appear only when populated:
+  //   • Pattern Name  — the item name (used when the Manufacturer # is blank so the item is still IDable)
+  //   • Your Ref / Tracking — the vendor's OWN order/confirmation # or shipment tracking # from their reply
+  const hasName = rows.some(r => r.name);
+  const hasRef = rows.some(r => r.ref);
+  const th = (t) => `<th style="padding:5px 18px 5px 0">${t}</th>`;
+  const td = (v, bold) => `<td style="padding:5px 18px 5px 0;white-space:nowrap">${bold ? '<strong>' : ''}${esc(v || '—')}${bold ? '</strong>' : ''}</td>`;
+  const headHtml = `<tr style="text-align:left;border-bottom:2px solid #333">${th('Date Ordered')}${th('Manufacturer #')}${hasName ? th('Pattern Name') : ''}${hasRef ? th('Your Ref / Tracking') : ''}</tr>`;
+  const rowsHtml = rows.map(r => `<tr style="border-bottom:1px solid #eee">${td(r.requested)}${td(r.mfr, true)}${hasName ? td(r.name) : ''}${hasRef ? td(r.ref) : ''}</tr>`).join('\n');
 
   const subject = `Sample Follow-Up — Outstanding Memos (Acct ${account}) — Designer Wallcoverings`;
   const s = vendor.ship_to;
@@ -28,10 +31,7 @@ Hopefully we removed any disco items internally from this list before asking aga
 <p><strong>Our account number is ${esc(account)}</strong></p>
 <p>We ordered 1 memo sample of each item below — please quote the Manufacturer # when you reply about an item.</p>
 <table style="border-collapse:collapse;font-size:14px;margin:8px 0">
-<tr style="text-align:left;border-bottom:2px solid #333">
-<th style="padding:5px 18px 5px 0">Date Ordered</th>
-<th style="padding:5px 0">Manufacturer #</th>
-</tr>
+${headHtml}
 ${rowsHtml}
 </table>
 <hr style="border:none;border-top:1px solid #ccc">
diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index c453105..6c53eb3 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -197,6 +197,40 @@ function georgeRecentRecipients(days = 14) {
   });
 }
 
+// Steve 9/01: enrich each item with the vendor's OWN order/confirmation # or shipment tracking #
+// from their reply that mentions THIS sku (shown as "Your Ref / Tracking"), and — when the mfr SKU
+// is blank — the pattern NAME. Best-effort + conservative: a ref is attached only when the vendor's
+// inbound mail actually names the sku AND carries a tracking/order number, so we never invent data.
+const _UPS = /\b1Z[0-9A-Z]{16}\b/, _FEDEX = /\b\d{12}\b|\b\d{15}\b|\b\d{20}\b/, _USPS = /\b9\d{21}\b/;
+const _ORDERNO = /\b(order|confirmation|conf\.?|sales ?order|web ?order|ref(?:erence)?)\s*#?\s*:?\s*([A-Za-z0-9][A-Za-z0-9\-]{3,})\b/i;
+const _reEsc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+function extractRef(text) {
+  const t = ' ' + text + ' ';
+  if (/track|fedex|ups|usps|shipped|tracking/i.test(t)) { const m = t.match(_UPS) || t.match(_FEDEX) || t.match(_USPS); if (m) return m[0] + ' (tracking)'; }
+  const o = t.match(_ORDERNO); if (o) return o[1].replace(/\.$/, '') + ' #' + o[2];
+  return '';
+}
+function georgeGet(path) {
+  const { auth } = georgeCreds();
+  return new Promise((res) => { http.get({ host: '127.0.0.1', port: 9850, path, headers: { Authorization: auth } }, (r) => { let b = ''; r.on('data', (d) => b += d); r.on('end', () => { try { res(JSON.parse(b)); } catch { res({}); } }); }).on('error', () => res({})); });
+}
+async function enrichRows(recipient, rows) {
+  const domain = String(recipient || '').split('@')[1] || '';
+  if (!domain) return;
+  for (const r of rows) {
+    if (!r.mfr || r.ref) continue;   // (blank-mfr name resolution needs a key we don't have without the sku)
+    try {
+      const j = await georgeGet(`/api/messages?account=info&maxResults=4&q=${encodeURIComponent(`in:inbox from:${domain} "${r.mfr}"`)}`);
+      for (const m of (j.messages || [])) {
+        const d = await georgeGet(`/api/messages/${m.id}?account=info`);
+        const text = String(d.body || '').replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/gi, ' ');
+        if (!new RegExp(_reEsc(r.mfr), 'i').test(text)) continue;   // must name THIS sku
+        const ref = extractRef(text); if (ref) { r.ref = ref; break; }
+      }
+    } catch {}
+  }
+}
+
 const run = async () => {
   const win = windowRange();
   const today = fmtDate(new Date());
@@ -247,7 +281,8 @@ const run = async () => {
     if (DRAFT) {
       const fresh = rows.filter((r) => !draftLedger[r.recordId]);   // only newly-outstanding SKUs
       if (!fresh.length) continue;                                   // already drafted — no dup
-      const draft = compose({ name: c.name, account_number: c.account_number, sample_email: c.sample_email, main_email: c.main_email, ship_to: SHIP_TO }, fresh.map((r) => ({ mfr: r.mfr, sku: r.sku, requested: r.requested })));
+      await enrichRows(c.sample_email || c.main_email, fresh);       // vendor ref/tracking when they responded
+      const draft = compose({ name: c.name, account_number: c.account_number, sample_email: c.sample_email, main_email: c.main_email, ship_to: SHIP_TO }, fresh.map((r) => ({ mfr: r.mfr, sku: r.sku, requested: r.requested, name: r.name, ref: r.ref })));
       // Flag (subject-only, visible in the Drafts list) when the team already emailed this vendor. Strip
       // the tag before sending — it's a review marker, not part of the vendor-facing letter.
       const flagPrefix = recentlyEmailed ? '[REVIEW — team emailed recently] ' : '';
@@ -258,7 +293,8 @@ const run = async () => {
       } else { skipped.push({ vid, n: fresh.length, reason: 'George draft failed: ' + res.detail }); }
       continue;
     }
-    const draft = compose({ name: c.name, account_number: c.account_number, sample_email: c.sample_email, main_email: c.main_email, ship_to: SHIP_TO }, rows.map((r) => ({ mfr: r.mfr, sku: r.sku, requested: r.requested })));
+    await enrichRows(c.sample_email || c.main_email, rows);         // vendor ref/tracking when they responded
+    const draft = compose({ name: c.name, account_number: c.account_number, sample_email: c.sample_email, main_email: c.main_email, ship_to: SHIP_TO }, rows.map((r) => ({ mfr: r.mfr, sku: r.sku, requested: r.requested, name: r.name, ref: r.ref })));
     if (!SEND) { sent.push({ vid, name: c.name, to: draft.to, n: rows.length, skus: rows.map((r) => r.sku), dryRun: true }); continue; }
     const res = await georgeSend({ account: 'info', to: draft.to, subject: draft.subject, body: draft.html });
     if (res.ok) {

← 0a260b3 sample-followup: drop DW ref# column, add explicit WIN overr  ·  back to Sample Followup Sweep  ·  sample-followup: drop the [REVIEW] subject prefix — flag liv 9a219d8 →