[object Object]

← back to Sample Followup Sweep

Add 60-day dead-lead ceiling + next vendor (Scalamandre) from live sweep

d8d9ad85f885ed0ba7906fba10a9264237354f17 · 2026-08-14 13:30:29 -0700 · Steve Abrams

Files touched

Diff

commit d8d9ad85f885ed0ba7906fba10a9264237354f17
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 14 13:30:29 2026 -0700

    Add 60-day dead-lead ceiling + next vendor (Scalamandre) from live sweep
---
 bin/run.js                    | 22 +++++++++++++---------
 data/vendors/scalamandre.json | 28 ++++++++++++++++++++++++++++
 lib/compose.js                |  5 +++--
 lib/sweep.js                  | 13 ++++++++-----
 4 files changed, 52 insertions(+), 16 deletions(-)

diff --git a/bin/run.js b/bin/run.js
index 2a836c6..2ce56f1 100644
--- a/bin/run.js
+++ b/bin/run.js
@@ -16,20 +16,21 @@ const today = new Date();
 // Phase 1 — sweep
 const { followUp, escalation, skipped } = sweep(vendor.rows, vendor.sweep_config || {}, today);
 
-// Phase 2 — resolve recipient
-if (!vendor.sample_email) {
-  console.error(`SKIP ${vendor.name}: no "Email address for Samples" on file — not sending to a guessed address.`);
-  process.exit(1);
-}
+// Phase 2 — resolve recipient (vendor-record fields). Missing = flag, don't guess.
+const missing = [];
+if (!vendor.sample_email) missing.push('sample_email (Email address for Samples)');
+if (!vendor.account_number) missing.push('account_number');
 
 // Phase 3 — compose
 const draft = compose(vendor, followUp);
 
-// Phase 4 — emit draft payload + preview (this JSON is exactly what gmail_create_draft consumes)
+// Phase 4 — emit preview always; only emit a sendable draft.json when the recipient is known.
 const outDir = path.join(root, 'out');
 fs.mkdirSync(outDir, { recursive: true });
-const payload = { account: 'info', to: draft.to, subject: draft.subject, body: draft.html };
-fs.writeFileSync(path.join(outDir, `${slug}.draft.json`), JSON.stringify(payload, null, 2));
+if (missing.length === 0) {
+  const payload = { account: 'info', to: draft.to, subject: draft.subject, body: draft.html };
+  fs.writeFileSync(path.join(outDir, `${slug}.draft.json`), JSON.stringify(payload, null, 2));
+}
 fs.writeFileSync(path.join(outDir, `${slug}.preview.html`), preview(vendor, draft, followUp, escalation, skipped));
 
 // Console summary
@@ -47,8 +48,11 @@ if (skipped.length) {
   console.log(`\nSkipped (${skipped.length}):`);
   skipped.forEach(s => console.log(`  - ${s.row.mfr}: ${s.reason}`));
 }
+if (missing.length) {
+  console.log(`\n⚠ NEEDS from vendor record before it can send: ${missing.join(', ')}`);
+}
 console.log(`\nArtifacts:`);
-console.log(`  out/${slug}.draft.json    → payload for gmail_create_draft (account=info)`);
+if (missing.length === 0) console.log(`  out/${slug}.draft.json    → payload for gmail_create_draft (account=info)`);
 console.log(`  out/${slug}.preview.html  → open in browser`);
 console.log(`\nPhase 5 (send) + Phase 6 (stamp 2nd Request = today) stay gated / manual.\n`);
 
diff --git a/data/vendors/scalamandre.json b/data/vendors/scalamandre.json
new file mode 100644
index 0000000..b07332a
--- /dev/null
+++ b/data/vendors/scalamandre.json
@@ -0,0 +1,28 @@
+{
+  "name": "Scalamandre",
+  "vid": "SCA",
+  "account_number": "",
+  "sample_email": "",
+  "ship_to": {
+    "name": "Designer Wallcoverings",
+    "line1": "15442 Ventura Blvd. #102",
+    "city_state_zip": "Sherman Oaks, CA 91403",
+    "phone": "1-888-373-4564"
+  },
+  "sweep_config": {
+    "minAgeDays": 10,
+    "maxAgeDays": 60,
+    "excludeDisco": true,
+    "requireNotReceived": true,
+    "secondRequestPolicy": "escalate"
+  },
+  "_source": "live fm_find REPORT ON SAMPLES ORDERED 2026-08-14, vid=SCA, outstanding & not disco",
+  "rows": [
+    { "mfr": "wh000023326", "dw": "DWA88956", "client": "Nikki Sturm", "entered": "2026-06-12" },
+    { "mfr": "sc_0008g1194", "dw": "DWA89306", "client": "Michelle Kungis", "entered": "2026-06-30" },
+    { "mfr": "SC WP88355D 0006", "dw": "DWA82596", "client": "Leah E Ramsay", "entered": "2026-07-11" },
+    { "mfr": "wh000013326", "dw": "DWA88957", "client": "GoldLeaf Interior", "entered": "2026-07-22" },
+    { "mfr": "wtt661518", "dw": "DWA88660", "client": "GoldLeaf Interior", "entered": "2026-07-22" },
+    { "mfr": "CL WP26420 0003", "dw": "DWA89455", "client": "Lisa McIlhenny", "entered": "2026-07-27" }
+  ]
+}
diff --git a/lib/compose.js b/lib/compose.js
index 4bf5973..819ce53 100644
--- a/lib/compose.js
+++ b/lib/compose.js
@@ -7,7 +7,8 @@ function esc(s) {
 }
 
 function compose(vendor, rows) {
-  const account = vendor.account_number;
+  const account = vendor.account_number || '‹CONFIRM DW ACCOUNT #›';
+  const to = vendor.sample_email || '‹CONFIRM SAMPLE EMAIL›';
   const lines = rows
     .map(r => (r.gap_before ? '<br>\n' : '') + esc(r.mfr))
     .join('<br>\n');
@@ -39,7 +40,7 @@ ${esc(s.phone)}
 <p>Best Regards,<br>Showroom Manager</p>
 </div>`;
 
-  return { to: vendor.sample_email, subject, html };
+  return { to, subject, html };
 }
 
 module.exports = { compose };
diff --git a/lib/sweep.js b/lib/sweep.js
index 75f93e3..3d9c125 100644
--- a/lib/sweep.js
+++ b/lib/sweep.js
@@ -13,6 +13,7 @@ function ageDays(entered, today) {
 function sweep(rows, opts = {}, today = new Date()) {
   const {
     minAgeDays = 10,
+    maxAgeDays = 60, // Steve 2026-08-14: >60 days = dead lead, don't chase
     excludeDisco = true,
     requireNotReceived = true,
     secondRequestPolicy = 'escalate', // 'escalate' (default A) | 'include'
@@ -24,14 +25,16 @@ function sweep(rows, opts = {}, today = new Date()) {
 
   for (const r of rows) {
     const age = ageDays(r.entered, today);
-    const overdue = r.manual_add === true || (age !== null && age > minAgeDays);
 
     if (excludeDisco && r.disco) { skipped.push({ row: r, reason: 'discontinued' }); continue; }
     if (requireNotReceived && r.received) { skipped.push({ row: r, reason: 'already received' }); continue; }
-    if (!overdue) {
-      skipped.push({ row: r, reason: age === null ? 'no entered date' : `only ${age}d old (<=${minAgeDays})` });
-      continue;
-    }
+
+    // Manual adds bypass the age window entirely (explicit human override).
+    if (r.manual_add === true) { followUp.push({ ...r, age }); continue; }
+
+    if (age === null) { skipped.push({ row: r, reason: 'no request date' }); continue; }
+    if (age <= minAgeDays) { skipped.push({ row: r, reason: `only ${age}d old (<=${minAgeDays})` }); continue; }
+    if (age > maxAgeDays) { skipped.push({ row: r, reason: `dead lead (${age}d > ${maxAgeDays})` }); continue; }
 
     if (r.second_request) {
       if (secondRequestPolicy === 'include') followUp.push({ ...r, age });

← a51c7d2 Sample Follow-Up Sweep — phases 1-4 engine + canary vendor (  ·  back to Sample Followup Sweep  ·  Fleet sweep (37 vendors/106 items) + selection console (serv d5b7022 →