← back to Sample Followup Sweep

scripts/resolve-original-dates.mjs

73 lines

#!/usr/bin/env node
// READ-ONLY verifier — for each outstanding memo item, resolve the ORIGINAL "New Sample Request"
// email date (from info@ Sent) by matching the item's manufacturer SKU (Mfr Pattern) in the email
// body, and print it next to the FileMaker `today for client` date so we can judge match quality
// BEFORE rewriting the customer-facing follow-up letter. Creates/sends NOTHING.
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import http from 'node:http';

const cfg = JSON.parse(readFileSync(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 g = (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 bauth = g('GEORGE_BASIC_AUTH'); if (!bauth.includes(':')) bauth = 'admin:' + g('GEORGE_BASIC_AUTH_PASS');
const A = 'Basic ' + Buffer.from(bauth).toString('base64');
const george = (p) => new Promise((res) => { http.get({ host: '127.0.0.1', port: 9850, path: p, headers: { Authorization: A } }, (x) => { let b = ''; x.on('data', (d) => b += d); x.on('end', () => { try { res(JSON.parse(b)); } catch { res({}); } }); }).on('error', () => res({})); });

const WIN = process.env.WIN || '07/03/2026...08/22/2026';
const pad = (n) => String(n).padStart(2, '0');
const fmt = (d) => `${pad(d.getMonth() + 1)}/${pad(d.getDate())}/${d.getFullYear()}`;
// normalize for SKU containment: lowercase, keep only alnum + . - /
const normKey = (s) => String(s || '').toLowerCase().replace(/&[a-z#0-9]+;/gi, ' ').replace(/[^a-z0-9./-]+/g, '');
// a SKU-like token from the mfr pattern (first run of >=5 chars of digits/dots/dashes/slashes)
const skuToken = (s) => { const m = String(s || '').match(/[a-z0-9][a-z0-9.\/-]{4,}/i); return m ? normKey(m[0]) : ''; };

// fetch all "New Sample Request" sent emails to a recipient → [{ms, bodyNorm, date}]
async function requestEmails(recip) {
  const q = encodeURIComponent(`in:sent to:${recip} subject:"New Sample Request"`);
  const j = await george(`/api/messages?account=info&maxResults=60&q=${q}`);
  const out = [];
  for (const m of (j.messages || [])) {
    const d = await george(`/api/messages/${m.id}?account=info`);
    const body = normKey((d.body || '').replace(/<[^>]+>/g, ' '));
    const ms = Date.parse(d.date || '') || 0;
    out.push({ ms, date: d.date || '', bodyNorm: body });
  }
  return out.sort((a, b) => a.ms - b.ms);   // earliest first
}
function originalDate(mfr, emails) {
  const key = normKey(mfr), tok = skuToken(mfr);
  for (const e of emails) {                                   // earliest-first → first hit = original
    if ((key.length >= 5 && e.bodyNorm.includes(key)) || (tok.length >= 5 && e.bodyNorm.includes(tok)))
      return { date: fmt(new Date(e.ms)), how: 'email' };
  }
  return null;
}

// outstanding items in the window
const A2 = (await fm.findRecords('WALLPAPER', 'REPORT ON SAMPLES ORDERED', { 'Date WP Sample Sent': '=', 'today for client': WIN }, { limit: 1200 })).records;
// group by vid, keep vendor recipient from contacts/fleet
const fleet = JSON.parse(readFileSync(new URL('../data/fleet.json', import.meta.url))).vendors;
const contacts = JSON.parse(readFileSync(new URL('../data/contacts.json', import.meta.url)));
const vidRecip = {}; for (const v of fleet) { const c = contacts[v.slug] || {}; const e = c.sample_email || c.main_email; if (e && !vidRecip[String(v.vid || '').toUpperCase()]) vidRecip[String(v.vid || '').toUpperCase()] = e; }

const byVid = {};
for (const r of A2) { const vid = String(r.fieldData.vid || '').replace(/[\r\n]/g, '').trim().toUpperCase(); if (!vid) continue; (byVid[vid] = byVid[vid] || []).push({ mfr: (r.fieldData['Mfr Pattern'] || '').trim(), fmDate: r.fieldData['today for client'] || '' }); }

let hit = 0, miss = 0;
for (const [vid, rows] of Object.entries(byVid)) {
  const recip = vidRecip[vid]; if (!recip) continue;
  const emails = await requestEmails(recip.split(/[,;]/)[0].trim());
  console.log(`\n[${vid}] ${recip}  (${emails.length} request-emails on file)`);
  for (const row of rows) {
    const od = originalDate(row.mfr, emails);
    if (od) hit++; else miss++;
    console.log(`   mfr ${String(row.mfr).slice(0, 30).padEnd(30)} | FM ${row.fmDate.padEnd(11)} | email ${od ? od.date : 'NO MATCH → keep FM date'}`);
  }
}
console.log(`\nmatched to an original email: ${hit}   |   no email match (fallback to FM date): ${miss}`);