[object Object]

← back to Sample Followup Sweep

sample-sender: main-email fallback + HELD-for-confirm guard + Anna French routing + 10-day/5yr window

dcd9edf16b757ead1957605807546191953b61ed · 2026-08-20 09:44:15 -0700 · Steve

Files touched

Diff

commit dcd9edf16b757ead1957605807546191953b61ed
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 20 09:44:15 2026 -0700

    sample-sender: main-email fallback + HELD-for-confirm guard + Anna French routing + 10-day/5yr window
---
 lib/compose.js            |  3 ++-
 scripts/scheduled-run.mjs | 34 ++++++++++++++++++++++++++--------
 2 files changed, 28 insertions(+), 9 deletions(-)

diff --git a/lib/compose.js b/lib/compose.js
index 819ce53..e5f1e64 100644
--- a/lib/compose.js
+++ b/lib/compose.js
@@ -8,7 +8,8 @@ function esc(s) {
 
 function compose(vendor, rows) {
   const account = vendor.account_number || '‹CONFIRM DW ACCOUNT #›';
-  const to = vendor.sample_email || '‹CONFIRM SAMPLE EMAIL›';
+  // 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›';
   const lines = rows
     .map(r => (r.gap_before ? '<br>\n' : '') + esc(r.mfr))
     .join('<br>\n');
diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index b3b0e6c..1a81d9b 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -8,7 +8,7 @@
 // run only sends the follow-ups that are NEW since the last run.
 //
 // Rules enforced:
-//   • window 10–60 days (>60 = dead lead, skip)      • Date WP Sample Sent empty (still outstanding)
+//   • age ≥ 10 days, NO upper cap (Steve 8/20: chase everything 10+ days, not the 10–60 band)  • Date WP Sample Sent empty (still outstanding)
 //   • Date Email Sent to Vendor after 10 Days EMPTY  → the "do not duplicate" guard
 //   • vendor must have a sample email + account       (else skip + report — can't send)
 //
@@ -32,7 +32,10 @@ const SEND = process.argv.includes('--send');
 const DB = 'WALLPAPER';
 const LAYOUT = 'Report for old memo samples';
 const FIELD_SENT = 'Date Email Sent to Vendor after 10 Days';
-const MIN_AGE_DAYS = 10, MAX_AGE_DAYS = 60;
+// Steve 8/20: 10-day threshold, drop the 60-day dead-lead cap — chase everything ≥10d not arrived.
+// True unlimited breaks the FileMaker date-range find (a lo-date >~5yr old returns 0 records), so
+// 1825d (5yr) is the widest window that reliably returns data = effectively "no cap" for sample memos.
+const MIN_AGE_DAYS = 10, MAX_AGE_DAYS = Number(process.env.MAXAGE || 1825);
 const SHIP_TO = { name: 'Designer Wallcoverings', line1: '15442 Ventura Blvd. #102', city_state_zip: 'Sherman Oaks, CA 91403', phone: '1-888-373-4564' };
 
 // --- FileMaker client (reuse the connector; creds from ~/.claude.json) ---
@@ -48,8 +51,11 @@ const fm = await import('/Users/macstudio3/Projects/filemaker-mcp/src/fm-client.
 const pad = (n) => String(n).padStart(2, '0');
 const fmtDate = (d) => `${pad(d.getMonth() + 1)}/${pad(d.getDate())}/${d.getFullYear()}`;
 function windowRange(today = new Date()) {
-  const lo = new Date(today); lo.setDate(lo.getDate() - MAX_AGE_DAYS);
   const hi = new Date(today); hi.setDate(hi.getDate() - MIN_AGE_DAYS);
+  // Steve 8/20: no upper age cap. Use an OPEN-ENDED low bound (FileMaker `...hi`
+  // = "on or before hi") so we chase EVERYTHING at least MIN_AGE_DAYS old.
+  if (MAX_AGE_DAYS >= 3650) return `...${fmtDate(hi)}`;
+  const lo = new Date(today); lo.setDate(lo.getDate() - MAX_AGE_DAYS);
   return `${fmtDate(lo)}...${fmtDate(hi)}`;
 }
 const norm = (s) => String(s || '').toLowerCase().trim();
@@ -64,8 +70,16 @@ function vidContactMap() {
     const c = contacts[v.slug] || {};
     const key = String(v.vid || '').toUpperCase();   // FileMaker vid case varies (yor vs YOR)
     if (c.disposition === 'no-chase') { map[key] = { noChase: true, name: v.name }; continue; }
-    if (c.sample_email && c.account_number && !map[key]) {
-      map[key] = { slug: v.slug, name: c.name || v.name, sample_email: c.sample_email, account_number: c.account_number };
+    // Steve 8/20: recipient = sample_email, else fall back to the vendor's main_email.
+    // A main_email fallback (or an entry flagged needs_confirm) rides a CONFIRM flag —
+    // it was resolved from recent correspondence, not a hard sample desk.
+    const email = c.sample_email || c.main_email;
+    if (email && c.account_number && !map[key]) {
+      map[key] = {
+        slug: v.slug, name: c.name || v.name,
+        sample_email: c.sample_email, main_email: c.main_email, account_number: c.account_number,
+        needs_confirm: !c.sample_email || !!c.needs_confirm,
+      };
     }
   }
   return map;
@@ -116,11 +130,14 @@ const run = async () => {
   }
 
   const cmap = vidContactMap();
-  const sent = [], skipped = [];
+  const sent = [], skipped = [], needsConfirm = [];
   for (const [vid, rows] of Object.entries(byVid)) {
     const c = cmap[vid];
     if (!c || c.noChase) { skipped.push({ vid, n: rows.length, reason: c?.noChase ? 'no-chase vendor' : 'no sample email/account on file' }); continue; }
-    const draft = compose({ name: c.name, account_number: c.account_number, sample_email: c.sample_email, ship_to: SHIP_TO }, rows.map((r) => ({ mfr: r.mfr })));
+    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 })));
+    // Steve 8/20: a fallback/resolved recipient (main_email, not a hard sample desk) NEVER
+    // auto-sends — the person must be confirmed against recent emails first. Hold it out.
+    if (c.needs_confirm) { needsConfirm.push({ vid, name: c.name, to: draft.to, n: rows.length, skus: rows.map((r) => r.sku), reason: 'main-email fallback — CONFIRM person vs recent emails before send' }); continue; }
     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) {
@@ -132,13 +149,14 @@ const run = async () => {
   }
 
   const report = { ranAt: new Date().toISOString(), mode: SEND ? 'SEND' : 'DRY-RUN', window: win, stampDate: today,
-    vendorsSent: sent.length, vendorsSkipped: skipped.length, sent, skipped };
+    vendorsSent: sent.length, vendorsSkipped: skipped.length, vendorsNeedsConfirm: needsConfirm.length, sent, needsConfirm, skipped };
   mkdirSync(join(ROOT, 'data', 'runs'), { recursive: true });
   writeFileSync(join(ROOT, 'data', 'runs', `scheduled-${today.replace(/\//g, '-')}.json`), JSON.stringify(report, null, 2));
 
   console.log(`Scheduled follow-up — ${report.mode} · window ${win} (${MIN_AGE_DAYS}-${MAX_AGE_DAYS}d) · dedup=FIELD empty\n`);
   console.log(`Would ${SEND ? 'HAVE SENT' : 'send'} to ${sent.length} vendor(s):`);
   sent.forEach((s) => console.log(`  ${SEND ? '✓' : '·'} ${s.name.padEnd(30)} [${s.vid}] → ${s.to}  (${s.n} SKU${s.n > 1 ? 's' : ''})`));
+  if (needsConfirm.length) { console.log(`\n⚠ ${needsConfirm.length} main-email fallback(s) — HELD for person-confirmation (never auto-sent):`); needsConfirm.forEach((s) => console.log(`  ? ${s.name.padEnd(30)} [${s.vid}] → ${s.to}  (${s.n} SKU${s.n > 1 ? 's' : ''}) — ${s.reason}`)); }
   if (skipped.length) { console.log(`\nSkipped ${skipped.length} (no contact / no-chase / error):`); skipped.forEach((s) => console.log(`  - [${s.vid}] ${s.n} SKU(s): ${s.reason}`)); }
   console.log(`\n→ data/runs/scheduled-${today.replace(/\//g, '-')}.json`);
 };

← 02128e4 auto-data-snapshot: 2026-08-20T09:38:44 (2 data files) — dat  ·  back to Sample Followup Sweep  ·  resolve 8 vendor sample desks from Gmail (held-for-confirm) 672dda2 →