← 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, '&').replace(/</g, '<').replace(/>/g, '>'); }
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 <info@designerwallcoverings.com></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">>${(v.sweep_config||{}).minAgeDays||10}d · disco-excluded · 2nd-req: ${(v.sweep_config||{}).secondRequestPolicy||'escalate'}</div>
</div>
</div></body></html>`;
}