[object Object]

← back to Pitch Guard

auto-save: 2026-06-30T05:58:45 (1 files) — server.js

4932ed3a1fdc3ed8fe734eccefb90dd54b363844 · 2026-06-30 05:58:50 -0700 · Steve Abrams

Files touched

Diff

commit 4932ed3a1fdc3ed8fe734eccefb90dd54b363844
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 30 05:58:50 2026 -0700

    auto-save: 2026-06-30T05:58:45 (1 files) — server.js
---
 server.js | 40 ++++++++++++++++++++++++++++++++--------
 1 file changed, 32 insertions(+), 8 deletions(-)

diff --git a/server.js b/server.js
index 3ca2004..280bb50 100644
--- a/server.js
+++ b/server.js
@@ -99,8 +99,10 @@ const BLOCK_LEVEL = 2;
 // evaluation. Steve's ask (2026-06-30): surface + sort by these to FIND who needs a
 // followup. They never block — an invoice we SENT is the opposite of a closed sale.
 const WARM_SIGNALS = [
-  { key: 'invoice_sent', label: 'INVOICE SENT', re: /\binvoice/i },
-  { key: 'samples_sent', label: 'SAMPLES SENT', re: /\b(samples?|swatch(?:es)?|memo sample|cutting)\b/i },
+  { key: 'invoice_sent',  label: 'INVOICE SENT',  re: /\binvoice/i },   // requires an attachment (enforced in detectWarmSent)
+  { key: 'samples_sent',  label: 'SAMPLES SENT',  re: /\b(samples?|swatch(?:es)?|memo sample|cutting)\b/i },
+  { key: 'stock_checked', label: 'STOCK CHECKED', re: /\b(in[\s-]?stock|in (our )?(warehouse|stock)|availabilit|available|on hand|lead[\s-]?time|currently (have|in stock)|we have (it|them|\d|in))\b/i },
+  { key: 'quote_sent',    label: 'PRICE QUOTED',  re: /\b(quote(d)?|pricing|your price|priced at|the price (is|would|comes)|per (yard|roll|sq|square)|\$\s?\d)\b/i },
 ];
 // ONLY these mean "already closed — leave them alone." 'invoiced' is intentionally NOT
 // here: an unpaid invoice we sent is a warm followup target, not a closed deal. paid /
@@ -138,19 +140,37 @@ async function assessRisk(email) {
 async function detectWarmSent(email) {
   email = String(email || '').toLowerCase();
   const found = {};
+  let quotedPrice = 0;
   try {
-    const q = `in:sent to:${email} (invoice OR sample OR samples OR swatch OR swatches OR "memo sample" OR cutting)`;
-    const r = await georgeRetry('/api/search', { query: { account: ACCOUNT, q, maxResults: 12 } });
+    // Broad warm-scan: samples / stock-checked / price-quoted. INVOICE is handled separately
+    // below because Steve's rule (2026-06-30) is that an invoice ONLY counts with an attachment.
+    const q = `in:sent to:${email} (sample OR samples OR swatch OR swatches OR "memo sample" OR cutting`
+      + ` OR stock OR availability OR available OR "lead time" OR "we have" OR "on hand"`
+      + ` OR quote OR quoted OR pricing OR price OR "per yard" OR "per roll")`;
+    const r = await georgeRetry('/api/search', { query: { account: ACCOUNT, q, maxResults: 15 } });
     for (const m of (r.messages || [])) {
       if (m.error) continue;
       const hay = `${m.subject || ''} ${m.snippet || ''}`;
       const ts = Date.parse(m.date) || 0;
       for (const s of WARM_SIGNALS) {
+        if (s.key === 'invoice_sent') continue;   // attachment-gated, handled below
         if (s.re.test(hay) && (!found[s.key] || ts > found[s.key].ts)) found[s.key] = { ts, date: m.date, label: s.label };
       }
+      // largest $ amount we quoted this client (for the Price sort)
+      const pm = hay.match(/\$\s?([\d,]+(?:\.\d{2})?)/g);
+      if (pm) for (const p of pm) { const v = parseFloat(p.replace(/[^\d.]/g, '')); if (v > quotedPrice) quotedPrice = v; }
+    }
+    // INVOICE — only when the sent email actually carries an attachment. Gmail-native
+    // has:attachment is the reliable check (search results don't expose MIME parts), so a
+    // bare "invoice" word with no PDF never trips INVOICE SENT (Steve 2026-06-30).
+    const ri = await georgeRetry('/api/search', { query: { account: ACCOUNT, q: `in:sent to:${email} invoice has:attachment`, maxResults: 5 } });
+    for (const m of (ri.messages || [])) {
+      if (m.error) continue;
+      const ts = Date.parse(m.date) || 0;
+      if (!found.invoice_sent || ts > found.invoice_sent.ts) found.invoice_sent = { ts, date: m.date, label: 'INVOICE SENT' };
     }
   } catch (e) { /* advisory — never block a candidate on a warm-scan failure */ }
-  return found;
+  return { signals: found, quotedPrice };
 }
 
 // Have we already SENT this contact a followup in the last 21 days? (in:sent only —
@@ -327,15 +347,19 @@ async function runCandidateScan() {
     // warm-lead signals: did WE send them an invoice or samples? Those are the
     // highest-priority follow targets (mid-funnel, about to be lost if not nudged).
     const warm = await detectWarmSent(contact);
-    const warmSignals = Object.values(warm).map(w => w.label);
+    const wsig = warm.signals || {};
+    const warmSignals = Object.values(wsig).map(w => w.label);
     candidates.push({
       threadId: info.msg.threadId,
       subject: info.msg.subject || '(no subject)',
       contact, contactName: displayName(info.msg.to) || contact,
       lastDate: info.msg.date, daysSince, uncertain,
       warmSignals,
-      sentInvoiceTs: warm.invoice_sent ? warm.invoice_sent.ts : 0,
-      sentSamplesTs: warm.samples_sent ? warm.samples_sent.ts : 0,
+      sentInvoiceTs: wsig.invoice_sent ? wsig.invoice_sent.ts : 0,
+      sentSamplesTs: wsig.samples_sent ? wsig.samples_sent.ts : 0,
+      sentStockTs:   wsig.stock_checked ? wsig.stock_checked.ts : 0,
+      sentQuoteTs:   wsig.quote_sent ? wsig.quote_sent.ts : 0,
+      quotedPrice:   warm.quotedPrice || 0,
     });
     await sleep(150);
   }

← aae4190 feature: surface invoice-sent & samples-sent as warm followu  ·  back to Pitch Guard  ·  (newest)