← 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)