[object Object]

← back to Filemaker Mcp

sales summary: report trailing 24h window via seen-ledger diff (Steve 2026-07-03)

1de626600385facc2f215ca5bc09aec772f3c558 · 2026-07-03 04:08:55 -0700 · Steve Abrams

Files touched

Diff

commit 1de626600385facc2f215ca5bc09aec772f3c558
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 3 04:08:55 2026 -0700

    sales summary: report trailing 24h window via seen-ledger diff (Steve 2026-07-03)
---
 scripts/sales-summary.mjs | 100 ++++++++++++++++++++++++++++++++++++----------
 1 file changed, 78 insertions(+), 22 deletions(-)

diff --git a/scripts/sales-summary.mjs b/scripts/sales-summary.mjs
index fa92a33..677a251 100644
--- a/scripts/sales-summary.mjs
+++ b/scripts/sales-summary.mjs
@@ -1,10 +1,17 @@
 // End-of-day sales recap → Slack #new-order, sourced from the FileMaker invoice DB.
-// Sums GRAND TOTAL 2 by invoice Date for TODAY and MONTH-TO-DATE (Pacific Time).
-// Reuses this project's FileMaker Cloud client (Claris ID auth) — no Shopify needed.
+// Reports a TRAILING 24-HOUR window: everything new since the previous run (Steve,
+// 2026-07-03 "Yes. Everything in last 24 hours at 4:30."). The invoice layout has no
+// timestamp fields — only dates — so the window is implemented as a diff against a
+// ledger of invoice numbers already reported (data/sales-summary-ledger.json). That
+// catches late-day orders, backdated entries, and quote→booked conversions exactly.
+// MONTH-TO-DATE stays bucketed by invoice Date (Pacific Time).
 //
-//   node scripts/sales-summary.mjs            # compute + POST to Slack
-//   node scripts/sales-summary.mjs --dry-run  # compute + print only (no post)
+//   node scripts/sales-summary.mjs            # compute + POST to Slack + update ledger
+//   node scripts/sales-summary.mjs --dry-run  # compute + print only (no post, no ledger write)
 
+import { readFileSync, writeFileSync, renameSync, existsSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
 import { findRecords } from '../src/fm-client.js';
 import { loadEnv, TZ, ptYMD, num, money } from '../lib/fm-script-helpers.js';
 
@@ -13,24 +20,43 @@ loadEnv(import.meta.url);
 const DB = 'invoice';
 const LAYOUT = 'GRAND TOTAL SALES';
 const TOTAL_FIELD = 'GRAND TOTAL 2';       // per-invoice grand total (merch + shipping + tax)
-const DATE_FIELD = 'Date';                 // invoice date
+const DATE_FIELD = 'Date';                 // invoice date (a DATE — the layout has no timestamps)
 const CHANNEL = process.env.SALES_SUMMARY_CHANNEL || 'C06D9C2PG1K'; // #new-order
 const DRY_RUN = process.argv.includes('--dry-run');
 const EXCLUDE_INVOICES = new Set(['999999']); // test/demo records
+const LEDGER_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', 'data', 'sales-summary-ledger.json');
+const LOOKBACK_DAYS = 10;   // how far back we pull to catch backdated entries
+const PRUNE_DAYS = 60;      // drop ledger entries not seen in this many days
 
 const pad = (n) => String(n).padStart(2, '0');
+const fmDate = ({ y, m, d }) => `${pad(m)}/${pad(d)}/${y}`; // FM returns zero-padded
+
+function loadLedger() {
+  if (!existsSync(LEDGER_PATH)) return null;
+  try { return JSON.parse(readFileSync(LEDGER_PATH, 'utf8')); } catch { return null; }
+}
+
+function saveLedger(ledger) {
+  const tmp = LEDGER_PATH + '.tmp';
+  writeFileSync(tmp, JSON.stringify(ledger, null, 1));
+  renameSync(tmp, LEDGER_PATH);
+}
 
 async function main() {
   const now = new Date();
   const { y, m, d } = ptYMD(now);
-  const monthStart = `${m}/1/${y}`;
-  const todayStr = `${m}/${d}/${y}`;
-  const todayFM = `${pad(m)}/${pad(d)}/${y}`; // FM returns zero-padded, e.g. 07/02/2026
+  const todayFM = fmDate({ y, m, d });
+  const yesterdayFM = fmDate(ptYMD(new Date(now.getTime() - 864e5)));
+  const todayISO = `${y}-${pad(m)}-${pad(d)}`;
+
+  // Pull window: month start (for MTD) or LOOKBACK_DAYS ago, whichever is earlier.
+  const lookback = ptYMD(new Date(now.getTime() - LOOKBACK_DAYS * 864e5));
+  const monthIsEarlier = y < lookback.y || (y === lookback.y && m < lookback.m);
+  const from = monthIsEarlier ? `${m}/1/${y}` : `${lookback.m}/${lookback.d}/${lookback.y}`;
 
-  // One MTD pull; bucket "today" client-side.
   const { records } = await findRecords(
     DB, LAYOUT,
-    [{ [DATE_FIELD]: `${monthStart}...${todayStr}` }],
+    [{ [DATE_FIELD]: `${from}...${m}/${d}/${y}` }],
     { limit: 2000, sort: [{ fieldName: DATE_FIELD, sortOrder: 'ascend' }] },
   );
 
@@ -40,19 +66,45 @@ async function main() {
     const p = String(f['PAID ON ACCOUNT'] ?? '').trim();
     return p !== '' && num(p) !== 0;
   };
+  const inCurrentMonth = (dateStr) => {
+    const [mm, , yy] = String(dateStr).split('/');
+    return Number(mm) === m && Number(yy) === y;
+  };
+
+  const ledger = loadLedger();
+  const firstRun = !ledger || !ledger.invoices;
+  const seen = firstRun ? {} : ledger.invoices;
+  const nextInvoices = { ...seen };
 
   let dayCount = 0, dayTotal = 0, mtdCount = 0, mtdTotal = 0, qCount = 0, qTotal = 0;
   for (const r of records) {
     const f = r.fieldData || {};
-    if (EXCLUDE_INVOICES.has(String(f['Invoice'] || '').trim())) continue;
+    const inv = String(f['Invoice'] || '').trim();
+    if (EXCLUDE_INVOICES.has(inv)) continue;
+    const key = inv || `rec:${r.recordId}`;
     const amt = num(f[TOTAL_FIELD]);
-    const isToday = String(f[DATE_FIELD] || '').trim() === todayFM;
-    if (isBooked(f)) {
-      mtdCount += 1; mtdTotal += amt;
-      if (isToday) { dayCount += 1; dayTotal += amt; }
-    } else if (isToday) {           // today's quotes with no money received yet
-      qCount += 1; qTotal += amt;
+    const dateStr = String(f[DATE_FIELD] || '').trim();
+    const booked = isBooked(f);
+
+    if (booked && inCurrentMonth(dateStr)) { mtdCount += 1; mtdTotal += amt; }
+
+    const prev = seen[key];
+    // First run has no ledger to diff against — approximate the 24h window as
+    // "dated yesterday or today". Every later run diffs against the ledger.
+    const inWindow = firstRun
+      ? (dateStr === todayFM || dateStr === yesterdayFM)
+      : (booked ? (!prev || prev.status !== 'booked') : !prev);
+    if (inWindow) {
+      if (booked) { dayCount += 1; dayTotal += amt; }
+      else { qCount += 1; qTotal += amt; }
     }
+    nextInvoices[key] = { status: booked ? 'booked' : 'quote', lastSeen: todayISO };
+  }
+
+  // Prune ledger entries not seen recently so the file stays small.
+  const cutoff = new Date(now.getTime() - PRUNE_DAYS * 864e5).toISOString().slice(0, 10);
+  for (const [key, v] of Object.entries(nextInvoices)) {
+    if ((v.lastSeen || '') < cutoff) delete nextInvoices[key];
   }
 
   const dayLabel = new Intl.DateTimeFormat('en-US', { timeZone: TZ, weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }).format(now);
@@ -60,12 +112,15 @@ async function main() {
 
   const text =
     `*📊 Daily Sales Summary — ${dayLabel}*\n\n` +
-    `🗓️ *Today — booked:* ${dayCount} order${dayCount === 1 ? '' : 's'} · *${money(dayTotal)}*\n` +
+    `🕓 *Last 24 hours — booked:* ${dayCount} order${dayCount === 1 ? '' : 's'} · *${money(dayTotal)}*\n` +
     `📈 *Month-to-date — booked (${monthLabel}):* ${mtdCount} order${mtdCount === 1 ? '' : 's'} · *${money(mtdTotal)}*\n` +
-    `📝 *Quotes today (no payment yet):* ${qCount} · ${money(qTotal)}\n\n` +
-    `_Booked = payment received (PAID ON ACCOUNT). Quotes = billed, no payment yet. Excludes test #999999; refunds net out._`;
+    `📝 *New quotes (last 24h):* ${qCount} · ${money(qTotal)}\n\n` +
+    `_Last 24h = everything new since the previous 4:30 report, incl. late-day + backdated orders and quotes that converted to booked. Booked = payment received (PAID ON ACCOUNT). Excludes test #999999; refunds net out._`;
 
-  if (DRY_RUN) { console.log('--- DRY RUN (not posted) ---\n' + text); return; }
+  if (DRY_RUN) {
+    console.log(`--- DRY RUN (not posted, ledger not written${firstRun ? '; first-run window = yesterday+today' : ''}) ---\n` + text);
+    return;
+  }
 
   const token = process.env.SLACK_BOT_TOKEN;
   if (!token) throw new Error('SLACK_BOT_TOKEN not set.');
@@ -76,7 +131,8 @@ async function main() {
   });
   const j = await res.json();
   if (!j.ok) throw new Error(`Slack post failed: ${j.error}`);
-  console.log(`✓ Posted to ${CHANNEL} (ts ${j.ts}) — Today ${dayCount}/${money(dayTotal)} · MTD ${mtdCount}/${money(mtdTotal)}`);
+  saveLedger({ lastRun: now.toISOString(), invoices: nextInvoices });
+  console.log(`✓ Posted to ${CHANNEL} (ts ${j.ts}) — 24h ${dayCount}/${money(dayTotal)} · MTD ${mtdCount}/${money(mtdTotal)}`);
 }
 
 main().catch((e) => { console.error('sales-summary error:', e.message); process.exit(1); });

← 70f934b daily sales summary: move fire time 23:55 -> 16:30 (4:30 PM  ·  back to Filemaker Mcp  ·  chore: macstudio3 migration — reconcile from mac2 + repoint 83d8494 →