← back to Butlr

scripts/ventura-hours-sweep.js

87 lines

// Sequential Ventura-Blvd "what are your hours" sweep.
// Places up to 40 calls ONE AT A TIME — each call must reach a terminal
// status (done/failed/canceled) before the next is placed. Live audio
// broadcasts on live.agentabrams.com as each call runs.
//
// Run on the Kamatera butlr host:
//   cd /root/public-projects/butlr && nohup node scripts/ventura-hours-sweep.js > /tmp/ventura_sweep.log 2>&1 &
//
// Stop it any time:  pkill -f ventura-hours-sweep

require('dotenv').config({ path: '/root/public-projects/butlr/.env' });
const fs = require('fs');
const data = require('../lib/data');
const twilio = require('../lib/twilio');

const USER_ID = 'rcA9SnnO';
const MAX_CALLS = 40;
const PER_CALL_TIMEOUT_MS = 130000;  // give a call up to ~2min to finish
const GAP_MS = 4000;                 // breath between calls
const TERMINAL = new Set(['done', 'failed', 'canceled', 'cancelled']);
const GOAL = 'Ask: what are your hours today? Wait for their answer, then thank them warmly and end the call. Ask nothing else; keep it under two sentences.';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function log(...a) { console.log(new Date().toISOString(), ...a); }

function shortLocality(addr) {
  // OSM display_name: "Name, number, street, neighborhood, city, county, state, zip, country"
  const parts = String(addr || '').split(',').map((s) => s.trim()).filter(Boolean);
  // drop the leading name part, keep number+street + city-ish
  const tail = parts.slice(1, 4).join(', ');
  return tail.slice(0, 60);
}

async function waitTerminal(id) {
  const start = Date.now();
  while (Date.now() - start < PER_CALL_TIMEOUT_MS) {
    const c = data.getCallUnscoped(id);
    if (c && TERMINAL.has(c.status)) return c.status;
    await sleep(3000);
  }
  return 'timeout';
}

(async () => {
  let list;
  try { list = JSON.parse(fs.readFileSync('/tmp/vall.json', 'utf8')); }
  catch (e) { log('FATAL: cannot read /tmp/vall.json —', e.message); process.exit(1); }
  list = list.slice(0, MAX_CALLS);
  log(`=== Ventura hours sweep: ${list.length} businesses, sequential ===`);

  let placed = 0, skipped = 0;
  for (let i = 0; i < list.length; i++) {
    const b = list[i];
    const loc = shortLocality(b.addr);
    const row = {
      business_name: (b.name + (loc ? ' — ' + loc : '')).slice(0, 120),
      business_phone: b.e164,
      goal: GOAL,
      callback_phone: '+16067130489',
      callback_name: 'Steve Adams',
      category: 'other',
      category_label: 'Ventura hours sweep',
      max_hold_minutes: 2,
      max_spend_cents: 50,
      consent_recording: 'on',
      consent_terms: 'on',
      notes: 'ventura 40 hours sweep ' + (i + 1) + '/' + list.length,
      allow_repeat: true,
    };
    const r = data.addCall(row, USER_ID);
    if (!r.ok) { skipped++; log(`SKIP ${i + 1}/${list.length} ${b.e164} ${b.name} — ${(r.errors || []).join('; ')}`); continue; }
    try {
      await twilio.dialCall(r.call);
      placed++;
      log(`PLACED ${i + 1}/${list.length} id=${r.call.id} ${b.e164} | ${b.name}`);
    } catch (e) {
      log(`DIAL-ERR ${i + 1}/${list.length} id=${r.call.id} — ${e.message}`);
      continue;
    }
    const status = await waitTerminal(r.call.id);
    log(`  └─ ${r.call.id} ended: ${status}`);
    await sleep(GAP_MS);
  }
  log(`=== sweep done: placed=${placed} skipped=${skipped} ===`);
  process.exit(0);
})();