← 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
M lib/compose.jsM scripts/scheduled-run.mjs
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 →