feat(sales-summaries): add a summary comparison tool, and report 30-day impact

compare-sales-summaries diffs summaries between two points in the same database
using as-of, so a recompute can be audited against exactly what was there before
without a snapshot or scratch copy.

It answers a question that balance-status counting hides: a day can stay
balanced while its line amounts move, which is a real change to the books even
though no red turns green. Amounts are compared at the cent so floating-point
noise does not read as a change.

Run over 2026-07-15 to 08-13 against production's own summaries at basis-t
209608347:

  out of balance -> balanced          309
  balanced -> out of balance            0
  balanced -> balanced, numbers moved 237
  no summary in production          4,058

Broken down by fix, neither calculation change touches a healthy day: R1 fixes
86 with 0 unbalanced and 0 balanced-day edits, R2 fixes 276 on the same terms.
Every one of the 237 balanced-but-changed days comes from the data work, which
is also the only step that unbalances anything — 19 days, all of them absorbed
by R1 and R2, which is why the end-to-end count is zero.

The 4,058 missing summaries include eight consecutive days, Jul 30 to Aug 6,
missing for all 210 clients — the coverage hole the plan predicted from the
scheduler's seven-day lookback, confirmed here independently from the data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 09:06:54 -07:00
parent f5f6602873
commit 7e4ff93b68
2 changed files with 555 additions and 0 deletions

View File

@@ -0,0 +1,420 @@
<title>Thirty-Day Reconciliation</title>
<style>
:root {
--paper: #F6F8F7;
--card: #FFFFFF;
--ink: #141F1D;
--ink-soft: #4A5C58;
--ink-faint: #7C8D89;
--rule: #DCE4E1;
--accent: #0E5B57;
--accent-soft: #E3EFED;
--good: #1A6B49;
--bad: #A03B26;
--warn: #8A6410;
--shadow: 0 1px 2px rgba(20,31,29,.06), 0 8px 24px rgba(20,31,29,.05);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--paper: #0E1615;
--card: #151F1E;
--ink: #E8EFED;
--ink-soft: #A3B3AF;
--ink-faint: #74847F;
--rule: #26332F;
--accent: #5FBDB4;
--accent-soft: #16302E;
--good: #5FBE8C;
--bad: #E08A72;
--warn: #D6AC55;
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.3);
}
}
:root[data-theme="dark"] {
--paper: #0E1615;
--card: #151F1E;
--ink: #E8EFED;
--ink-soft: #A3B3AF;
--ink-faint: #74847F;
--rule: #26332F;
--accent: #5FBDB4;
--accent-soft: #16302E;
--good: #5FBE8C;
--bad: #E08A72;
--warn: #D6AC55;
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.3);
}
* { box-sizing: border-box; }
body {
background: var(--paper);
color: var(--ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 16px;
line-height: 1.6;
margin: 0;
padding: 0 20px 96px;
}
.wrap { max-width: 940px; margin: 0 auto; }
.measure { max-width: 66ch; }
.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
.mono {
font-family: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", monospace;
font-variant-numeric: tabular-nums;
}
header.masthead {
padding: 72px 0 40px;
border-bottom: 2px solid var(--ink);
display: flex;
flex-direction: column;
gap: 14px;
}
.eyebrow {
font-size: 12px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--accent);
font-weight: 600;
}
h1 {
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
font-size: clamp(34px, 5.4vw, 54px);
line-height: 1.08;
font-weight: 600;
letter-spacing: -.015em;
margin: 0;
text-wrap: balance;
}
.standfirst { font-size: 19px; color: var(--ink-soft); margin: 0; max-width: 60ch; }
.meta {
display: flex; flex-wrap: wrap; gap: 10px 28px;
font-size: 13px; color: var(--ink-faint);
padding-top: 6px;
}
.meta b { color: var(--ink-soft); font-weight: 600; }
section { padding-top: 56px; display: flex; flex-direction: column; gap: 20px; }
h2 {
font-family: Georgia, "Iowan Old Style", serif;
font-size: 27px; font-weight: 600; letter-spacing: -.01em;
margin: 0; text-wrap: balance;
}
h3 {
font-size: 14px; letter-spacing: .08em; text-transform: uppercase;
color: var(--ink-soft); font-weight: 700; margin: 0;
}
p { margin: 0; }
.measure p + p { margin-top: 14px; }
/* headline before/after */
.ledger {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: stretch;
gap: 0;
border: 1px solid var(--rule);
border-radius: 4px;
background: var(--card);
box-shadow: var(--shadow);
overflow: hidden;
}
.ledger > div { padding: 26px 28px; display: flex; flex-direction: column; gap: 6px; }
.ledger .arrow {
justify-content: center; align-items: center;
border-left: 1px solid var(--rule); border-right: 1px solid var(--rule);
color: var(--ink-faint); font-size: 22px; padding: 26px 18px;
background: var(--accent-soft);
}
.ledger .side-label {
font-size: 12px; letter-spacing: .12em; text-transform: uppercase;
color: var(--ink-faint); font-weight: 600;
}
.figure { font-size: clamp(28px, 4.4vw, 40px); font-weight: 650; line-height: 1.05; letter-spacing: -.02em; }
.figure.after { color: var(--good); }
.subfig { font-size: 14px; color: var(--ink-soft); }
/* stat row */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px; }
.stat {
background: var(--card); border: 1px solid var(--rule); border-radius: 4px;
padding: 18px 20px; display: flex; flex-direction: column; gap: 4px;
}
.stat .k { font-size: 30px; font-weight: 650; letter-spacing: -.02em; line-height: 1; }
.stat .l { font-size: 13px; color: var(--ink-soft); }
.stat.zero .k { color: var(--good); }
.stat.flag .k { color: var(--warn); }
/* steps */
.step { border-left: 2px solid var(--rule); padding-left: 26px; position: relative; }
.step::before {
content: attr(data-step);
position: absolute; left: -13px; top: 2px;
width: 24px; height: 24px; border-radius: 50%;
background: var(--accent); color: var(--paper);
font-size: 12px; font-weight: 700;
display: grid; place-items: center;
}
.step h3 { margin-bottom: 8px; }
.step-title { font-size: 20px; font-weight: 650; letter-spacing: -.01em; }
.scroll { overflow-x: auto; border: 1px solid var(--rule); border-radius: 4px; background: var(--card); }
table { border-collapse: collapse; width: 100%; font-size: 14.5px; }
th, td { padding: 11px 16px; text-align: left; border-bottom: 1px solid var(--rule); white-space: nowrap; }
thead th {
font-size: 11.5px; letter-spacing: .09em; text-transform: uppercase;
color: var(--ink-faint); font-weight: 700; background: var(--accent-soft);
}
tbody tr:last-child td { border-bottom: none; }
td.n, th.n { text-align: right; font-variant-numeric: tabular-nums; }
tr.total td { font-weight: 650; background: var(--accent-soft); }
.good { color: var(--good); font-weight: 650; }
.bad { color: var(--bad); font-weight: 650; }
.dim { color: var(--ink-faint); }
.pill {
display: inline-block; font-size: 11.5px; font-weight: 700;
letter-spacing: .05em; text-transform: uppercase;
padding: 3px 9px; border-radius: 3px;
}
.pill.ok { background: var(--accent-soft); color: var(--good); }
.pill.no { background: var(--accent-soft); color: var(--bad); }
.callout {
background: var(--card); border: 1px solid var(--rule);
border-left: 3px solid var(--accent);
border-radius: 4px; padding: 20px 24px;
display: flex; flex-direction: column; gap: 10px;
}
.callout.warn { border-left-color: var(--warn); }
.callout .h { font-weight: 650; }
code {
font-family: ui-monospace, "SF Mono", monospace; font-size: .9em;
background: var(--accent-soft); padding: 1px 5px; border-radius: 3px;
}
footer {
margin-top: 72px; padding-top: 24px; border-top: 1px solid var(--rule);
font-size: 13px; color: var(--ink-faint);
display: flex; flex-direction: column; gap: 8px;
}
ul { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 8px; }
</style>
<div class="wrap">
<header class="masthead">
<div class="eyebrow">Sales summaries · restored production backup</div>
<h1>Thirty-Day Reconciliation</h1>
<p class="standfirst">What the deduplication work and the two calculation fixes actually did to the books, measured day by day against production's own summaries.</p>
<div class="meta">
<span><b>Window</b> 2026-07-15 → 2026-08-13</span>
<span><b>Client-days</b> <span class="num">3,040</span> with activity</span>
<span><b>Compared against</b> basis-t <span class="num">209608347</span></span>
</div>
</header>
<section>
<div class="ledger">
<div>
<span class="side-label">Baseline</span>
<span class="figure num">$22,527.40</span>
<span class="subfig"><span class="num">398</span> days out of balance · <span class="num">86.00%</span> clean</span>
</div>
<div class="arrow" aria-hidden="true"></div>
<div>
<span class="side-label">After fixes</span>
<span class="figure after num">$405.66</span>
<span class="subfig"><span class="num">67</span> days out of balance · <span class="num">97.64%</span> clean</span>
</div>
</div>
<div class="stats">
<div class="stat"><span class="k num">331</span><span class="l">client-days brought into balance</span></div>
<div class="stat zero"><span class="k num">0</span><span class="l">days knocked out of balance</span></div>
<div class="stat flag"><span class="k num">237</span><span class="l">already-balanced days whose numbers moved</span></div>
<div class="stat"><span class="k num">98.2%</span><span class="l">of the dollar variance removed</span></div>
</div>
<div class="measure">
<p>Both arms are computed on the same deduplicated data, so this isolates what the <em>calculation</em> fixes are worth. Figures exclude the ten deactivated duplicate clients, which the plan says to exclude from reporting; including them the shape is identical (432 → 70 days, $25,622.98 → $1,548.64).</p>
</div>
</section>
<section>
<h2>Was anything already balanced disturbed?</h2>
<div class="measure">
<p>This is the question that matters most for the books, and it has two halves. No day that balanced under production went out of balance — that count is zero. But <strong>237 client-days that were balanced had their line amounts change anyway</strong>, and a balance-only view would hide every one of them.</p>
<p>None of those 237 came from the calculation fixes. Both fixes are provably inert on balanced days — see the decomposition below. They came from the data being corrected underneath.</p>
</div>
<div class="scroll">
<table>
<thead>
<tr><th>Outcome, production → final</th><th class="n">Client-days</th><th>Reading</th></tr>
</thead>
<tbody>
<tr><td>Out of balance → balanced</td><td class="n good">309</td><td class="dim">the intended repair</td></tr>
<tr><td>Balanced → out of balance</td><td class="n good">0</td><td class="dim">nothing was broken</td></tr>
<tr><td>Balanced → balanced, numbers changed</td><td class="n" style="color:var(--warn);font-weight:650">237</td><td class="dim">amounts moved, balance held</td></tr>
<tr><td>Balanced → balanced, identical</td><td class="n">1,652</td><td class="dim">untouched</td></tr>
<tr><td>Still out of balance</td><td class="n">44</td><td class="dim">residual, see below</td></tr>
<tr class="total"><td>Production summaries in window</td><td class="n">2,242</td><td class="dim">of 6,300 client-day slots</td></tr>
</tbody>
</table>
</div>
<h3>What moved on those 237 days</h3>
<div class="scroll">
<table>
<thead><tr><th>Pattern</th><th class="n">Days</th><th>Cause</th></tr></thead>
<tbody>
<tr><td>Every category 0.00 → real values</td><td class="n">~100</td><td>production held an all-zero summary for a day that had orders</td></tr>
<tr><td>Card Payments ↔ Fees reallocation</td><td class="n">~135</td><td>the payout arrived, so the processing fee is now known and booked separately</td></tr>
<tr><td>Service Charges line appears</td><td class="n">13</td><td>R2, on days that were already out of balance</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p>The second pattern is worth reading carefully: <code>Card Payments</code> falls by exactly what <code>Fees</code> gains, so the day stays balanced while the split between the two lines changes. That is a data-freshness effect from re-importing, not a change in how anything is calculated.</p>
</div>
</section>
<section>
<h2>Impact by underlying fix</h2>
<div class="measure">
<p>Applied in sequence, each measured against the state before it. Percentages of a 3,040 client-day population.</p>
</div>
<div class="step" data-step="1">
<h3>Data</h3>
<div class="step-title">Deduplication — Phase 0, re-key, re-import</div>
</div>
<div class="scroll">
<table>
<thead><tr><th>Outcome</th><th class="n">Client-days</th></tr></thead>
<tbody>
<tr><td>Unchanged</td><td class="n">1,581</td></tr>
<tr><td>Numbers changed</td><td class="n">547</td></tr>
<tr><td>Out of balance → balanced</td><td class="n good">56</td></tr>
<tr><td>Balanced → out of balance</td><td class="n bad">19</td></tr>
<tr><td>Balanced → balanced, numbers changed</td><td class="n" style="color:var(--warn);font-weight:650">237</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p>The data work is the only step that unbalances anything: <strong>19 days</strong> go out of balance purely from correcting the data, mostly where a refund that had been sitting under the twin client now lands on the right one. The two calculation fixes then absorb all 19, which is why the end-to-end figure is zero.</p>
</div>
<div class="step" data-step="2">
<h3>R1</h3>
<div class="step-title">Tips on untendered orders</div>
</div>
<div class="scroll">
<table>
<thead><tr><th>Outcome</th><th class="n">Client-days</th><th class="n">Share</th></tr></thead>
<tbody>
<tr><td>Unchanged</td><td class="n">2,941</td><td class="n dim">96.7%</td></tr>
<tr><td>Out of balance → balanced</td><td class="n good">86</td><td class="n dim">2.8%</td></tr>
<tr><td>Balanced → out of balance</td><td class="n good">0</td><td class="n dim"></td></tr>
<tr><td>Balanced → balanced, numbers changed</td><td class="n good">0</td><td class="n dim"></td></tr>
<tr><td>Still out of balance, but closer</td><td class="n">13</td><td class="n dim">0.4%</td></tr>
<tr class="total"><td>Days touched · dollars moved</td><td class="n">99</td><td class="n">$973.79</td></tr>
</tbody>
</table>
</div>
<div class="step" data-step="3">
<h3>R2</h3>
<div class="step-title">Square service charges, both signs</div>
</div>
<div class="scroll">
<table>
<thead><tr><th>Outcome</th><th class="n">Client-days</th><th class="n">Share</th></tr></thead>
<tbody>
<tr><td>Unchanged</td><td class="n">2,760</td><td class="n dim">90.8%</td></tr>
<tr><td>Out of balance → balanced</td><td class="n good">276</td><td class="n dim">9.1%</td></tr>
<tr><td>Balanced → out of balance</td><td class="n good">0</td><td class="n dim"></td></tr>
<tr><td>Balanced → balanced, numbers changed</td><td class="n good">0</td><td class="n dim"></td></tr>
<tr><td>Still out of balance, but closer</td><td class="n">4</td><td class="n dim">0.1%</td></tr>
<tr class="total"><td>Days touched · dollars moved</td><td class="n">280</td><td class="n">$23,729.45</td></tr>
</tbody>
</table>
</div>
<div class="callout">
<span class="h">Both calculation fixes are inert on healthy days</span>
<p>Across 3,040 client-days, neither R1 nor R2 changed a single number on a day that was already balanced. Every day they touched was already out of balance. That is the strongest available evidence that they cannot quietly restate correct books — and it is why R2 carries the larger dollar figure without carrying larger risk.</p>
</div>
</section>
<section>
<h2>Days production never had</h2>
<div class="measure">
<p>Of 6,300 client-day slots in the window, production held a summary for only 2,242. The remaining <strong>4,058</strong> had none at all — nothing to compare against, and nothing an accountant could have looked at.</p>
<p>They are not evenly spread. Eight consecutive days show <span class="num">210</span> missing summaries each, which is <em>every client in the database</em>:</p>
</div>
<div class="scroll">
<table>
<thead><tr><th>Dates</th><th class="n">Missing per day</th><th>Reading</th></tr></thead>
<tbody>
<tr><td>Jul 15 Jul 29</td><td class="n">117</td><td class="dim">clients with no summary that day</td></tr>
<tr><td class="bad">Jul 30 Aug 6</td><td class="n bad">210</td><td class="bad">every client — a total coverage hole</td></tr>
<tr><td>Aug 7 Aug 13</td><td class="n">89</td><td class="dim">clients with no summary that day</td></tr>
</tbody>
</table>
</div>
<div class="callout warn">
<span class="h">The coverage hole is real and self-confirming</span>
<p>The plan predicted a global gap at 2026-07-30 → 08-06 from reading the scheduler, which only looks back seven days and so can never backfill a hole older than that. This measurement found the same eight days independently, from the data. Every one of those 1,680 client-days now has a summary.</p>
</div>
</section>
<section>
<h2>What is left</h2>
<div class="measure">
<p>Five client-days above the ten-cent materiality threshold, totalling $404.04. Everything else — 62 days — comes to <strong>$1.62</strong>, with the largest single day at nine cents.</p>
</div>
<div class="scroll">
<table>
<thead><tr><th>Client</th><th>Date</th><th class="n">Variance</th><th>Explanation</th></tr></thead>
<tbody>
<tr><td class="mono">NGBK</td><td>2026-08-06</td><td class="n">+$299.42</td><td>refunds now held by both twins — a re-key consequence</td></tr>
<tr><td class="mono">NGDA</td><td>2026-08-01</td><td class="n">$50.00</td><td>auto-gratuity booked as a service charge</td></tr>
<tr><td class="mono">NGEB</td><td>2026-08-10</td><td class="n">$25.00</td><td>ezCater fee semantics <span class="pill ok">predicted</span></td></tr>
<tr><td class="mono">NGEB</td><td>2026-07-29</td><td class="n">$20.00</td><td>ezCater fee semantics <span class="pill ok">predicted</span></td></tr>
<tr><td class="mono">NGPS</td><td>2026-08-12</td><td class="n">+$9.62</td><td>unexplained <span class="pill ok">predicted</span></td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p>The ten-cent threshold separates register rounding from real variance with nothing sitting near the boundary — the largest sub-threshold day is 9.00¢ and the smallest material one is $9.62, two orders of magnitude apart.</p>
</div>
</section>
<section>
<h2>How this was measured</h2>
<div class="measure">
<ul>
<li>Run against a production backup restored to basis-t <span class="num">209608347</span>, newest transaction 2026-08-14 22:52. Nothing in production was touched.</li>
<li>"Before" is production's own stored summaries, read via <code>as-of</code> — not a re-simulation of them. Datomic keeps every past value, so the comparison is against exactly what was there.</li>
<li>A day counts as out of balance when debits minus credits is at least half a cent. Line amounts are compared at the cent, so floating-point noise does not read as a change.</li>
<li>R1 and R2 only ever add credits, so baseline is derived as <code>fixed + untendered tip + service charges</code>. That identity was checked against a from-scratch baseline recomputation on 20 random client-days and agreed on every one.</li>
<li>All 14,458 summaries in the database were recomputed through the real job, not a test harness.</li>
</ul>
</div>
<div class="callout warn">
<span class="h">Two defects still open before this ships</span>
<p>Re-keying a charge that has two parent orders duplicates the tender, because the other client's import then matches neither key and creates a second charge which cardinality-many appends. The migration must split shared charges first. Separately, <code>remove-voided-orders</code> retracts orders, and 63% of the contended clients' charges have two parents, so a retraction there deletes the other client's payment.</p>
</div>
</section>
<footer>
<span>Measured 2026-08-15 on <span class="mono">integreat-prod-restore</span>. Branch <span class="mono">worktree-sales-summary-balance</span>.</span>
<span>Service charges map to account 49000 Service Income, chosen to make measurement possible and still pending accounting sign-off. The account affects only whether a day can reach accepted — never whether it balances — so no figure here depends on it.</span>
</footer>
</div>