Files
integreat/docs/2026-08-15-thirty-day-reconciliation.html
Bryce 7e4ff93b68 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>
2026-08-15 09:06:54 -07:00

421 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>