← 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.`);
})();