← back to Commercialrealestate
scripts/deal-alerts.js
55 lines
// deal-alerts.js — P1 retention hook (docs/TOOL-SPEC.md): for each user's saved searches, find NEW
// deals (added since the search's last_seen) that match its filters, and send a digest. $0, local.
// The digest is the reason a loan officer opens the tool daily. Email send is best-effort via a mailer
// (send-magic-link.js pattern / George) when CRCP_MAIL=1; otherwise writes the digest to
// data/deal-alerts-out/<email>-<date>.txt so nothing is lost and no send fires unbidden.
// node scripts/deal-alerts.js
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const ACC = path.join(ROOT, 'data', 'crcp-accounts.json');
const FLOW = path.join(ROOT, 'data', 'deals-flow.json');
const OUTDIR = path.join(ROOT, 'data', 'deal-alerts-out');
const load = f => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
const today = new Date().toISOString().slice(0, 10);
function matches(d, f) {
if (f.src && f.src.length && !f.src.includes('LA County Assessor')) return false; // county-only feed here
if (f.use && f.use.length && !f.use.includes(d.use)) return false;
if (f.pMin != null && !(+d.price >= f.pMin)) return false;
if (f.uMin != null && !(+d.units >= f.uMin)) return false;
if (f.q) { const t = String(f.q).toLowerCase(); if (![d.address, d.use, d.year].some(v => v != null && String(v).toLowerCase().includes(t))) return false; }
return true;
}
(function main() {
const db = load(ACC); const flow = load(FLOW);
if (!db || !db.saved || !flow) { console.log('no accounts/saved or no deal data — nothing to do.'); return; }
const deals = flow.deals || [];
fs.mkdirSync(OUTDIR, { recursive: true });
let digests = 0, changed = false;
for (const [email, searches] of Object.entries(db.saved)) {
const blocks = [];
for (const s of searches) {
const since = s.last_seen || '2000-01-01';
const hits = deals.filter(d => (d.date || '') > since && matches(d, s.filters || {}));
if (hits.length) {
hits.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
blocks.push(`● "${s.name}" — ${hits.length} new since ${since}\n` +
hits.slice(0, 15).map(d => ` ${d.date} $${(+d.price || 0).toLocaleString().padStart(12)} ${(d.use || '').padEnd(12)} ${d.address || d.ain}`).join('\n'));
}
s.last_seen = today; changed = true; // advance the watermark
}
if (blocks.length) {
const body = `CRCP Deal Flow — new matches for your saved searches (${today})\n\n${blocks.join('\n\n')}\n\nView: ${process.env.CRCP_BASE || 'https://crcp.agentabrams.com'}/deals-flow.html`;
const out = path.join(OUTDIR, `${email.replace(/[^a-z0-9]/gi, '_')}-${today}.txt`);
fs.writeFileSync(out, body);
if (process.env.CRCP_MAIL === '1') { try { require('child_process').execFile('node', [path.join(__dirname, 'send-magic-link.js'), email, body], () => {}); } catch (_) {} }
digests++;
}
}
if (changed) { const t = ACC + '.tmp'; fs.writeFileSync(t, JSON.stringify(db, null, 2)); fs.renameSync(t, ACC); }
console.log(`deal-alerts: ${digests} digest(s) generated (${process.env.CRCP_MAIL === '1' ? 'emailed + ' : ''}written to data/deal-alerts-out/). $0 local.`);
})();