← back to Sample Followup Sweep

bin/run.js

97 lines

'use strict';
// Phases 1–4 orchestrator for one vendor.
//   node bin/run.js <vendor-slug>
// Emits out/<slug>.draft.json (payload for gmail_create_draft) + out/<slug>.preview.html

const fs = require('fs');
const path = require('path');
const { sweep } = require('../lib/sweep');
const { compose } = require('../lib/compose');
const { createGmailDraftArtifact } = require('../lib/george-transport');

const slug = process.argv[2] || 'osborne-little';
const root = path.join(__dirname, '..');
const vendor = JSON.parse(fs.readFileSync(path.join(root, 'data', 'vendors', `${slug}.json`), 'utf8'));
const today = new Date();

// Phase 1 — sweep
const { followUp, escalation, skipped } = sweep(vendor.rows, vendor.sweep_config || {}, today);

// Phase 2 — resolve recipient (vendor-record fields). Missing = flag, don't guess.
const missing = [];
if (!vendor.sample_email) missing.push('sample_email (Email address for Samples)');
if (!vendor.account_number) missing.push('account_number');

// Phase 3 — compose
const draft = compose(vendor, followUp);

// Phase 4 — emit preview always; only emit a sendable draft.json when the recipient is known.
const outDir = path.join(root, 'out');
fs.mkdirSync(outDir, { recursive: true });
if (missing.length === 0) {
  const payload = createGmailDraftArtifact({ account: 'info', to: draft.to, subject: draft.subject, body: draft.html });
  fs.writeFileSync(path.join(outDir, `${slug}.draft.json`), JSON.stringify(payload, null, 2));
}
fs.writeFileSync(path.join(outDir, `${slug}.preview.html`), preview(vendor, draft, followUp, escalation, skipped));

// Console summary
const line = (r) => `  • ${r.mfr}  [${r.dw || '-'}]  ${r.age != null ? r.age + 'd' : 'manual add'}`;
console.log(`\n=== Sample Follow-Up Sweep — ${vendor.name} ===`);
console.log(`Recipient (sample email): ${draft.to}`);
console.log(`Subject: ${draft.subject}`);
console.log(`\nFollow-up rows (${followUp.length}):`);
followUp.forEach(r => console.log(line(r)));
if (escalation.length) {
  console.log(`\nEscalation — already 2nd-requested (${escalation.length}):`);
  escalation.forEach(r => console.log(`  ⚠ ${r.mfr}  [${r.dw}]  existing 2nd req ${r.second_request}`));
}
if (skipped.length) {
  console.log(`\nSkipped (${skipped.length}):`);
  skipped.forEach(s => console.log(`  - ${s.row.mfr}: ${s.reason}`));
}
if (missing.length) {
  console.log(`\n⚠ NEEDS from vendor record before it can send: ${missing.join(', ')}`);
}
console.log(`\nArtifacts:`);
if (missing.length === 0) console.log(`  out/${slug}.draft.json    → payload for gmail_create_draft (account=info)`);
console.log(`  out/${slug}.preview.html  → open in browser`);
console.log(`\nPhase 5 (send) + Phase 6 (stamp 2nd Request = today) stay gated / manual.\n`);

function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }

function preview(v, d, fu, escl, sk) {
  const rowsPanel = fu.map(r => `<li>${esc(r.mfr)} <span class="dim">[${esc(r.dw || '-')}] ${r.age != null ? r.age + 'd' : 'manual'}</span></li>`).join('');
  const esclPanel = escl.length ? `<h4>Escalation — already 2nd-requested</h4><ul>${escl.map(r => `<li>${esc(r.mfr)} <span class="dim">2nd req ${esc(r.second_request)}</span></li>`).join('')}</ul>` : '';
  const skPanel = sk.length ? `<h4>Skipped</h4><ul>${sk.map(s => `<li>${esc(s.row.mfr)} <span class="dim">${esc(s.reason)}</span></li>`).join('')}</ul>` : '';
  return `<!doctype html><html><head><meta charset="utf-8"><title>Sweep — ${esc(v.name)}</title>
<style>
 body{background:#f2f2f2;margin:0;padding:24px;font-family:Arial,Helvetica,sans-serif;color:#222}
 .wrap{max-width:1040px;margin:0 auto;display:grid;grid-template-columns:1fr 320px;gap:18px}
 .card{background:#fff;border:1px solid #ddd;border-radius:8px;padding:24px 28px;box-shadow:0 1px 4px rgba(0,0,0,.06)}
 .card p{font-size:14px;line-height:1.5;margin:0 0 12px}
 .env{font-size:13px;color:#555;margin-bottom:14px}
 .env b{color:#222}
 hr{border:none;border-top:1px solid #ccc;margin:14px 0}
 .side{background:#fff;border:1px solid #ddd;border-radius:8px;padding:16px 18px;font-size:13px;height:fit-content}
 .side h3{margin:0 0 4px} .side h4{margin:14px 0 4px;color:#444}
 .side ul{margin:0;padding-left:18px} .side li{margin:2px 0;line-height:1.35}
 .dim{color:#999;font-size:12px}
 .status{color:#b8860b;font-weight:bold;margin-bottom:10px}
</style></head><body><div class="wrap">
 <div class="card">
  <div class="status">DRAFT — staged in info@ → Drafts (not sent). This is exactly what sends.</div>
  <div class="env"><div><b>From:</b> Designer Wallcoverings &lt;info@designerwallcoverings.com&gt;</div>
   <div><b>To:</b> ${esc(d.to)}</div><div><b>Subject:</b> ${esc(d.subject)}</div></div>
  <hr>${d.html}
 </div>
 <div class="side">
  <h3>Sweep result</h3>
  <div class="dim">${esc(v.name)} · acct ${esc(v.account_number)}</div>
  <h4>Follow-up (${fu.length})</h4><ul>${rowsPanel}</ul>
  ${esclPanel}${skPanel}
  <h4>Config</h4>
  <div class="dim">&gt;${(v.sweep_config||{}).minAgeDays||10}d · disco-excluded · 2nd-req: ${(v.sweep_config||{}).secondRequestPolicy||'escalate'}</div>
 </div>
</div></body></html>`;
}