From ab57a3e135add9f335e2e66838285a2e8e2b6acb Mon Sep 17 00:00:00 2001 From: louispaulb Date: Mon, 1 Jun 2026 19:57:09 -0400 Subject: [PATCH] fix(reports/legacy): freshness from service date, not invoice date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marc Robidoux flagged as overpriced (129.95$) — he has a loyalty discount (service id 74448) that should lower it. Investigation: 74448 doesn't exist in the copy (its max service id is 74393), so the discount was added after the snapshot. Same freshness issue as Julie Dupuis — not a calc bug. But this also exposed that the freshness banner was wrong: it read the newest INVOICE date (Apr 30) while the snapshot actually carries SERVICES created through May 22 — May's recurring billing run simply hadn't executed at dump time, so invoices lag services by ~3 weeks. For a report that reads active services/plans/discounts, the service date is the right freshness signal. fetchDataAsOf now returns both {services, invoices}; data_as_of (shown in the banner) is the service date (May 22), with last_invoice kept for reference. The copy is ~10 days stale, not ~1 month. Marc's loyalty credit still won't show until the copy is refreshed (task #38). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) --- services/targo-hub/lib/legacy-reports.js | 37 +++++++++++++++++------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/services/targo-hub/lib/legacy-reports.js b/services/targo-hub/lib/legacy-reports.js index 46e1ae3..7c817ac 100644 --- a/services/targo-hub/lib/legacy-reports.js +++ b/services/targo-hub/lib/legacy-reports.js @@ -176,17 +176,29 @@ function buildQuery (url) { return { sql, params, meta: { threshold, segment, includeAddons, cats, limit } } } -// Freshness probe — the most recent invoice date in the legacy copy tells -// us how stale the data is. The copy is a one-shot snapshot (no auto-sync), -// so the report must advertise its as-of date to avoid misleading the -// operator into acting on month-old prices (a re-negotiated client like -// Julie Dupuis won't show their new discount until the copy is refreshed). +// Freshness probe — the copy is a one-shot snapshot (no auto-sync), so the +// report advertises its as-of date so nobody acts on stale prices. +// +// This report reads SERVICES (active plans/discounts), not invoices, so the +// relevant freshness is the newest service.date_orig — NOT the newest +// invoice date. They differ: the snapshot carries services created up to +// ~May 22 but invoices only through Apr 30 (May's recurring billing run +// hadn't happened at dump time). Using the invoice date understated +// freshness by ~3 weeks. We return both; the UI shows the service one. +// (A discount added after the snapshot — e.g. Marc Robidoux's loyalty +// credit, service id 74448 > the copy's max 74393 — still won't appear +// until the copy is refreshed.) async function fetchDataAsOf (pool) { + const out = { services: null, invoices: null } try { - const [r] = await pool.execute('SELECT MAX(date_orig) AS max_ts FROM invoice') - const ts = r?.[0]?.max_ts - return ts ? new Date(ts * 1000).toISOString() : null - } catch { return null } + const [s] = await pool.execute('SELECT MAX(date_orig) AS max_ts FROM service') + if (s?.[0]?.max_ts) out.services = new Date(s[0].max_ts * 1000).toISOString() + } catch { /* ignore */ } + try { + const [i] = await pool.execute('SELECT MAX(date_orig) AS max_ts FROM invoice') + if (i?.[0]?.max_ts) out.invoices = new Date(i[0].max_ts * 1000).toISOString() + } catch { /* ignore */ } + return out } async function handleJson (req, res, url) { @@ -196,12 +208,15 @@ async function handleJson (req, res, url) { try { const t0 = Date.now() const [rows] = await pool.execute(sql, params) - const dataAsOf = await fetchDataAsOf(pool) + const asOf = await fetchDataAsOf(pool) log(`legacy report overpriced-internet: ${rows.length} rows in ${Date.now() - t0}ms (threshold=${meta.threshold}, segment=${meta.segment}, addons=${meta.includeAddons})`) return json(res, 200, { threshold: meta.threshold, segment: meta.segment, include_addons: meta.includeAddons, categories: meta.cats, - data_as_of: dataAsOf, + // data_as_of = service freshness (the relevant one for this report); + // last_invoice kept for reference. + data_as_of: asOf.services, + last_invoice: asOf.invoices, count: rows.length, rows, }) } catch (e) {