[object Object]

← back to Filemaker Mcp

Reports bot: visible truncation warning + stop counting refunds as orders

bbd74734d886be2047ad6498bf2bac339dd8987a · 2026-07-28 20:20:36 -0700 · Steve Abrams

Final DTD verdict FIX-THEN-SHIP (5/5) after contrarian gate. Two live-repro'd fixes:
- Truncation now posts a degraded '⚠️ DATA TRUNCATED' Slack warning (via FM
  foundCount>returnedCount) instead of throwing and going dark — silence was
  indistinguishable from a healthy zero-order day. Ledger is not written on a
  truncated run so the 24h diff isn't poisoned.
- Negative refund rows netted the dollar total correctly but were inflating the
  ORDER COUNT (14-16 rows). Count now excludes refunds; totals still net them out.
  Footer reworded to match (dropped the misleading 'refunds net out' phrasing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit bbd74734d886be2047ad6498bf2bac339dd8987a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 28 20:20:36 2026 -0700

    Reports bot: visible truncation warning + stop counting refunds as orders
    
    Final DTD verdict FIX-THEN-SHIP (5/5) after contrarian gate. Two live-repro'd fixes:
    - Truncation now posts a degraded '⚠️ DATA TRUNCATED' Slack warning (via FM
      foundCount>returnedCount) instead of throwing and going dark — silence was
      indistinguishable from a healthy zero-order day. Ledger is not written on a
      truncated run so the 24h diff isn't poisoned.
    - Negative refund rows netted the dollar total correctly but were inflating the
      ORDER COUNT (14-16 rows). Count now excludes refunds; totals still net them out.
      Footer reworded to match (dropped the misleading 'refunds net out' phrasing).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/sales-summary.mjs | 53 +++++++++++++++++++++++++++++++++--------------
 1 file changed, 37 insertions(+), 16 deletions(-)

diff --git a/scripts/sales-summary.mjs b/scripts/sales-summary.mjs
index 92eb4a6..150ca55 100644
--- a/scripts/sales-summary.mjs
+++ b/scripts/sales-summary.mjs
@@ -43,6 +43,19 @@ function saveLedger(ledger) {
   renameSync(tmp, LEDGER_PATH);
 }
 
+async function postSlack(text) {
+  const token = process.env.SLACK_BOT_TOKEN;
+  if (!token) throw new Error('SLACK_BOT_TOKEN not set.');
+  const res = await fetch('https://slack.com/api/chat.postMessage', {
+    method: 'POST',
+    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json; charset=utf-8' },
+    body: JSON.stringify({ channel: CHANNEL, text, unfurl_links: false }),
+  });
+  const j = await res.json();
+  if (!j.ok) throw new Error(`Slack post failed: ${j.error}`);
+  return j;
+}
+
 async function main() {
   const now = new Date();
   const { y, m, d } = ptYMD(now);
@@ -59,16 +72,30 @@ async function main() {
   const lookbackCrossedMonth = lookback.y !== y || lookback.m !== m;
   const from = lookbackCrossedMonth ? `${lookback.m}/${lookback.d}/${lookback.y}` : `${m}/1/${y}`;
 
-  const { records } = await findRecords(
+  const { records, dataInfo } = await findRecords(
     DB, LAYOUT,
     [{ [DATE_FIELD]: `${from}...${m}/${d}/${y}` }],
     { limit: FETCH_LIMIT, sort: [{ fieldName: DATE_FIELD, sortOrder: 'ascend' }] },
   );
 
   // Guard: a silently-truncated fetch is the same failure class as the old MTD
-  // window bug — it would undercount without warning. Fail LOUD if we ever hit the cap.
-  if (records.length >= FETCH_LIMIT) {
-    throw new Error(`Fetch hit FETCH_LIMIT (${FETCH_LIMIT}) for window ${from}..${m}/${d}/${y} — MTD would truncate. Raise FETCH_LIMIT or paginate before trusting this run.`);
+  // window bug — it would undercount without warning. FileMaker hands us the exact
+  // signal (foundCount > returnedCount); the length===cap check is a fallback.
+  // Fail VISIBLE, not dark: post a degraded warning (silence reads as a healthy
+  // zero-order day) and stop WITHOUT writing the ledger (incomplete data must not
+  // poison the 24h diff).
+  const foundCount = Number(dataInfo?.foundCount ?? records.length);
+  const returnedCount = Number(dataInfo?.returnedCount ?? records.length);
+  if (foundCount > returnedCount || records.length >= FETCH_LIMIT) {
+    const warn =
+      `*⚠️ Daily Sales Summary — DATA TRUNCATED, numbers unreliable*\n\n` +
+      `The invoice fetch for ${from}–${m}/${d}/${y} returned ${returnedCount} of ${foundCount} matching records ` +
+      `(page cap ${FETCH_LIMIT}). Month-to-date would undercount, so no figures are reported this run.\n` +
+      `_Fix: raise FETCH_LIMIT or paginate in sales-summary.mjs, then re-run._`;
+    if (DRY_RUN) { console.log(`--- DRY RUN (truncation guard tripped) ---\n` + warn); return; }
+    const j = await postSlack(warn);
+    console.log(`⚠️ Truncation warning posted to ${CHANNEL} (ts ${j.ts}); ledger NOT written.`);
+    return;
   }
 
   // A BOOKED order (= a real sale) has money received (PAID ON ACCOUNT ≠ 0).
@@ -97,7 +124,9 @@ async function main() {
     const dateStr = String(f[DATE_FIELD] || '').trim();
     const booked = isBooked(f);
 
-    if (booked && inCurrentMonth(dateStr)) { mtdCount += 1; mtdTotal += amt; }
+    // Refunds post as negative-amount booked rows: they net out of the dollar
+    // total (amt is added) but must NOT inflate the ORDER count (amt > 0 only).
+    if (booked && inCurrentMonth(dateStr)) { if (amt > 0) mtdCount += 1; mtdTotal += amt; }
 
     const prev = seen[key];
     // First run has no ledger to diff against — approximate the 24h window as
@@ -106,7 +135,7 @@ async function main() {
       ? (dateStr === todayFM || dateStr === yesterdayFM)
       : (booked ? (!prev || prev.status !== 'booked') : !prev);
     if (inWindow) {
-      if (booked) { dayCount += 1; dayTotal += amt; }
+      if (booked) { if (amt > 0) dayCount += 1; dayTotal += amt; }
       else { qCount += 1; qTotal += amt; }
     }
     nextInvoices[key] = { status: booked ? 'booked' : 'quote', lastSeen: todayISO };
@@ -126,22 +155,14 @@ async function main() {
     `🕓 *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` +
     `📝 *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._`;
+    `_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). Order counts exclude refunds; dollar totals net refunds out. Excludes test #999999._`;
 
   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.');
-  const res = await fetch('https://slack.com/api/chat.postMessage', {
-    method: 'POST',
-    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json; charset=utf-8' },
-    body: JSON.stringify({ channel: CHANNEL, text, unfurl_links: false }),
-  });
-  const j = await res.json();
-  if (!j.ok) throw new Error(`Slack post failed: ${j.error}`);
+  const j = await postSlack(text);
   saveLedger({ lastRun: now.toISOString(), invoices: nextInvoices });
   console.log(`✓ Posted to ${CHANNEL} (ts ${j.ts}) — 24h ${dayCount}/${money(dayTotal)} · MTD ${mtdCount}/${money(mtdTotal)}`);
 }

← 073ebdd Harden sales-summary against silent fetch truncation  ·  back to Filemaker Mcp  ·  Reports bot: correct MTD bucketing comment to match date-onl 541942f →