← back to Sample Followup Sweep

scripts/send-reconciled.mjs.bak-20260923-131934-TK12085

100 lines

#!/usr/bin/env node
// Reconciled sample-follow-up DRAFTER (Steve 8/20).
// Pulls outstanding memos in the "fill-in-since-last-send / 10-day-floor" window and DRAFTS one
// letter per vendor into info@ Drafts (NEVER auto-sends). Steve reviews + sends from Drafts.
// Letter = intro + "Attn / processed by: <vendor processor>" + Ref#·Date Ordered·Manufacturer# table.
//   WIN env overrides the FileMaker date range (default = today's fill-in window).
import { readFileSync, writeFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const ROOT = join(homedir(), 'Projects/sample-followup-sweep');
const { georgeRequest } = require(join(ROOT, 'lib/george-transport.js'));
// Rolling window: memos that crossed the 10-day follow-up floor within the last CATCHUP days (default 7),
// so a daily scheduled run always covers any gap. Env WIN overrides for a manual one-off. (Draft-dedup
// below stops the same memo being drafted twice across runs.)
const _pad = (n) => String(n).padStart(2, '0');
const _fmt = (d) => `${_pad(d.getMonth() + 1)}/${_pad(d.getDate())}/${d.getFullYear()}`;
// Steve 2026-09-22 (TK-12013/TK-12014): full active window [today-MAX_AGE .. today-MIN_AGE] inclusive,
// NOT a narrow rolling catch-up band — so no older still-outstanding memo is ever dropped. Draft-dedup
// (reconciled-draft-ledger keyed by vendor+combo-sku) makes re-covering already-drafted memos a no-op.
const _MIN_AGE = 10, _MAX_AGE = Number(process.env.MAX_AGE) || 60;
const _hi = new Date(); _hi.setDate(_hi.getDate() - _MIN_AGE);   // includes exactly-10d cohort (inclusive range)
const _lo = new Date(); _lo.setDate(_lo.getDate() - _MAX_AGE);   // 60-day dead-lead floor
const WIN = process.env.WIN || `${_fmt(_lo)}...${_fmt(_hi)}`;

// --- FileMaker: pull outstanding (Date WP Sample Sent empty) in WIN, group by vid ---
const cfg = JSON.parse(readFileSync(join(homedir(), '.claude.json'), 'utf8'));
const fenv = cfg?.mcpServers?.filemaker?.env || {};
for (const k of ['FM_CLOUD_HOST', 'FM_CLARIS_EMAIL', 'FM_CLARIS_PASSWORD']) process.env[k] = fenv[k];
process.env.FM_READONLY = '1';
const fm = await import('/Users/macstudio3/Projects/filemaker-mcp/src/fm-client.js');
const A = (await fm.findRecords('WALLPAPER', 'REPORT ON SAMPLES ORDERED',
  { 'Date WP Sample Sent': '=', 'today for client': WIN }, { limit: 1200 }).catch(() => ({ records: [] }))).records;
const snap = {};
for (const r of A) { const d = r.fieldData; const v = String(d.vid || '').toUpperCase(); if (!v) continue;
  (snap[v] = snap[v] || []).push({ mfr: (d['Mfr Pattern'] || '').trim(), sku: d['combo sku'] || '', req: d['today for client'] || '' }); }
writeFileSync(join(ROOT, 'data/outstanding-snapshot.json'), JSON.stringify(snap, null, 2));

const esc = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const SHIP = { name: 'Designer Wallcoverings', line1: '15442 Ventura Blvd. #102', csz: 'Sherman Oaks, CA 91403', phone: '1-888-373-4564' };
// Sent-verified addresses + processor (person at vendor who handled the order), per vendor
const V = {
  YOR:    { name: 'York', to: 'RiveraH@yorkwall.com, orders@yorkwall.com', acct: '34328', proc: 'York Sample Dept (order-thru-website desk)' },
  BRE:    { name: 'Brewster', to: 'brewsterorders@brewp.com', acct: '6003784', proc: 'Brewster Orders desk' },
  BMGREEN:{ name: 'Greenland / bmwallpaper', to: 'sales@bmwallpaper.com', acct: 'On File', proc: 'Nora (NORA & GREENLAND)' },
  OSB:    { name: 'Osborne & Little', to: 'Polancol@oalusa.com', acct: '1433803', proc: 'Leandra Polanco' },
  KRA:    { name: 'Kravet', to: 'matt.schoffman@kravet.com', acct: '10087117', proc: 'Matt Schoffman' },
  ROMO:   { name: 'Romo', to: 'fawn.bowen@romo.com', acct: '223641', proc: 'Fawn Bowen (Sampling Coordinator)' },
  SANDB:  { name: 'Sandberg (via Gimmersta)', to: 'support@gimmersta.com', acct: 'On File', proc: 'Alva' },
  WQ:     { name: 'WallQuest / Malibu', to: 'orders@wallquest.com', acct: '651317', proc: 'WallQuest Orders desk' },
  THIB:   { name: 'Thibaut', to: 'cathy.dy@thibautdesign.com', acct: '0108751', proc: 'Cathy Dy' },
  ARTE:   { name: 'Arte (@ Egg & Dart)', to: 'Therese@egg-and-dart.com', acct: 'On File', proc: 'Therese Stachowiak' },
};
// merge BM + GREEN under one bmwallpaper letter
const items = {};
for (const [vid, arr] of Object.entries(snap)) { const key = (vid === 'BM' || vid === 'GREEN') ? 'BMGREEN' : vid; (items[key] = items[key] || []).push(...arr); }

function letter(v, rows) {
  // Steve 8/20: do NOT show our internal Ref # (DW SKU) to the vendor — Date Ordered + Mfr # only.
  const tbl = rows.map(r => `<tr style="border-bottom:1px solid #eee"><td style="padding:5px 18px 5px 0;white-space:nowrap">${esc(r.req || '—')}</td><td style="padding:5px 0">${esc(r.mfr)}</td></tr>`).join('\n');
  return `<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#222;line-height:1.5">
<p>We are following up on memo samples we have not yet received (ordered ~10 days ago).</p>
<p><strong>Our account number is ${esc(v.acct)}</strong></p>
<p><strong>Attn / processed by:</strong> ${esc(v.proc)} — please route these to whoever handled the original order.</p>
<table style="border-collapse:collapse;font-size:14px;margin:8px 0">
<tr style="text-align:left;border-bottom:2px solid #333"><th style="padding:5px 18px 5px 0">Date Ordered</th><th style="padding:5px 0">Manufacturer #</th></tr>
${tbl}
</table>
<p>Ship to:<br>${esc(SHIP.name)}<br>${esc(SHIP.line1)}<br>${esc(SHIP.csz)}<br>${esc(SHIP.phone)}</p>
<p><strong>Sidemark: Samples ASAP</strong></p>
<p>Thank you!<br>Showroom Manager</p>
<p style="font-size:11px;color:#666">Designer Wallcoverings · 15442 Ventura Blvd. #102 · Sherman Oaks, CA 91403<br>
To stop receiving sample follow-up emails, <a href="mailto:info@designerwallcoverings.com?subject=Unsubscribe%20from%20sample%20follow-ups">unsubscribe here</a>.</p></div>`;
}
// George draft (account=info, /api/drafts — no send-approval token)
const genv = k => { for (const f of ['/Users/macstudio3/Projects/Designer-Wallcoverings/DW-MCP/.env', '/Users/macstudio3/Projects/george-gmail/.env']) { try { const m = readFileSync(f, 'utf8').match(new RegExp('^' + k + '=(.+)$', 'm')); if (m) return m[1].trim(); } catch {} } return ''; };
let auth = genv('GEORGE_BASIC_AUTH'); if (!auth.includes(':')) auth = 'admin:' + genv('GEORGE_BASIC_AUTH_PASS');
auth = 'Basic ' + Buffer.from(auth).toString('base64');
async function draft(to, subject, body) {
  const result = await georgeRequest({ path: '/api/drafts', payload: { account: 'info', to, subject, body }, headers: { Authorization: auth } });
  try { const parsed = JSON.parse(result.body); return parsed.success ? 'ok' : result.body.slice(0, 80); } catch { return result.body.slice(0, 80); }
}

const DO = process.argv.includes('--draft');
// Draft-dedup: never re-draft a memo (keyed by vendor+combo-sku) that a prior run already drafted,
// so a daily scheduled run over a rolling window can't pile up duplicate drafts.
const LEDGER = join(ROOT, 'data/reconciled-draft-ledger.json');
let ledger = {}; try { ledger = JSON.parse(readFileSync(LEDGER, 'utf8')); } catch {}
const stamp = _fmt(new Date());
console.log(`Reconciled follow-up — window ${WIN} — ${DO ? 'DRAFT into info@' : 'DRY-RUN (add --draft to create)'}\n`);
for (const [key, rows] of Object.entries(items)) {
  const v = V[key]; if (!v) { console.log(`  ? no vendor map for ${key} (${rows.length})`); continue; }
  const fresh = rows.filter(r => !ledger[`${key}:${r.sku}`]);
  if (!fresh.length) { console.log(`  ⊘ ${v.name.padEnd(26)} — all ${rows.length} already drafted (skip)`); continue; }
  const subj = `Sample Follow-Up — Outstanding Memos (Acct ${v.acct}) — Designer Wallcoverings`;
  if (!DO) { console.log(`  · ${v.name.padEnd(26)} → ${v.to.padEnd(40)} (${fresh.length}) refs: ${fresh.map(r => r.sku).join(', ')}`); continue; }
  const r = await draft(v.to, subj, letter(v, fresh));
  console.log(`  ${r === 'ok' ? '✎' : '✗'} ${v.name.padEnd(26)} → ${v.to.padEnd(40)} (${fresh.length}) ${r === 'ok' ? '' : r}`);
  if (r === 'ok') for (const row of fresh) ledger[`${key}:${row.sku}`] = stamp;
}
if (DO) writeFileSync(LEDGER, JSON.stringify(ledger, null, 2));