The report asserted that the fixes work; it did not show what changed on the page. Replaces the worked-examples section with actual summary lines pulled from the restore, before and after, including debit and credit totals. NGLK 2026-08-04 is the clearest case. The day already carried a $60.00 card refund — the guest was given their money back, tip included — while the Tip line still credited $482.94, because the reversal sits on an order with no tender and get-tip only reached tips through tenders. Corrected to $422.94, matching the refund to the cent, and the day closes from -60.00 to 0.00. NTPT 2026-08-06 shows the other shape: $427.10 of service charge arrived inside a $4,975.89 card tender, so it landed on the debit side with nothing crediting it. Credits go 7,350.10 to 7,777.20 against unchanged debits of 7,777.20. Adds the six largest repairs of each kind with their real Tip and Service Charges figures, and NGNP 2026-06-25 where both fixes land on one day and pull in opposite directions — 301.40 credited, 1.80 removed, 299.60 closed — which is a useful check that the two are independent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
534 lines
32 KiB
HTML
534 lines
32 KiB
HTML
<title>Ninety-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", 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; }
|
||
|
||
.ledger {
|
||
display: grid; grid-template-columns: 1fr auto 1fr; align-items: stretch;
|
||
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); }
|
||
|
||
.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); }
|
||
|
||
.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); }
|
||
|
||
.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; }
|
||
pre { margin: 0; padding: 20px; font-size: 13px; line-height: 1.7; white-space: pre; font-family: ui-monospace, "SF Mono", monospace; }
|
||
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>Ninety-Day Reconciliation</h1>
|
||
<p class="standfirst">A true baseline: the job re-run three times over ninety days — production's calculation, then each fix added — so every number below is measured output, not arithmetic.</p>
|
||
<div class="meta">
|
||
<span><b>Window</b> 2026-05-10 → 2026-08-07</span>
|
||
<span><b>Client-days</b> <span class="num">8,733</span> with activity</span>
|
||
<span><b>Clients</b> <span class="num">106</span></span>
|
||
<span><b>Most recent week</b> omitted</span>
|
||
</div>
|
||
</header>
|
||
|
||
<section>
|
||
<div class="ledger">
|
||
<div>
|
||
<span class="side-label">Baseline</span>
|
||
<span class="figure num">$58,531.75</span>
|
||
<span class="subfig"><span class="num">1,024</span> days out of balance · <span class="num">87.74%</span> clean</span>
|
||
</div>
|
||
<div class="arrow" aria-hidden="true">→</div>
|
||
<div>
|
||
<span class="side-label">After both fixes</span>
|
||
<span class="figure after num">$852.38</span>
|
||
<span class="subfig"><span class="num">105</span> days out of balance · <span class="num">98.74%</span> clean</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats">
|
||
<div class="stat"><span class="k num">919</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 zero"><span class="k num">0</span><span class="l">balanced days whose numbers moved</span></div>
|
||
<div class="stat"><span class="k num">98.5%</span><span class="l">of the dollar variance removed</span></div>
|
||
</div>
|
||
|
||
<div class="measure">
|
||
<p>Figures exclude the ten deactivated duplicate clients, which the plan says to exclude from reporting. Across the full 8,733 client-days including them the shape is the same: 1,087 → 108 days out of balance, $63,764.92 → $1,995.36.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Why this baseline is different</h2>
|
||
<div class="measure">
|
||
<p>The earlier thirty-day report derived its baseline arithmetically. This one does not. The job was run three separate times over the same ninety days, against the same data, writing real summaries each time:</p>
|
||
<ul>
|
||
<li><strong>Run A — baseline.</strong> The service-charge flag cleared on all 210 clients and <code>get-tip</code> restored to its tendered-only form. This is exactly what production calculates today.</li>
|
||
<li><strong>Run B — plus R1.</strong> Only <code>get-tip</code> changed.</li>
|
||
<li><strong>Run C — plus R2.</strong> The service-charge flag switched on as well.</li>
|
||
</ul>
|
||
<p>The underlying data — Phase 0, the re-key, the repaired charge references — is identical across all three, so what separates them is the calculation and nothing else. 18,900 summaries were written per run, 56,700 in total.</p>
|
||
<p>The most recent week (08-08 → 08-14) is excluded deliberately. The backup was cut mid-evening on the 14th, and the days either side of that are partial, which distorted the earlier window.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Impact by underlying fix</h2>
|
||
<div class="measure">
|
||
<p>Applied in sequence, each measured against the run before it, over 8,350 reportable client-days.</p>
|
||
</div>
|
||
|
||
<div class="step" data-step="1">
|
||
<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">8,054</td><td class="n dim">96.5%</td></tr>
|
||
<tr><td>Out of balance → balanced</td><td class="n good">272</td><td class="n dim">3.3%</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">24</td><td class="n dim">0.3%</td></tr>
|
||
<tr class="total"><td>Days touched · dollars moved</td><td class="n">296</td><td class="n">$3,623.44</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="step" data-step="2">
|
||
<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">7,701</td><td class="n dim">92.2%</td></tr>
|
||
<tr><td>Out of balance → balanced</td><td class="n good">647</td><td class="n dim">7.7%</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">2</td><td class="n dim"><0.1%</td></tr>
|
||
<tr class="total"><td>Days touched · dollars moved</td><td class="n">649</td><td class="n">$54,775.79</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h3>Both together, full population</h3>
|
||
<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">7,749</td></tr>
|
||
<tr><td>Out of balance → balanced</td><td class="n good">979</td></tr>
|
||
<tr><td>Balanced → out of balance</td><td class="n good">0</td></tr>
|
||
<tr><td>Balanced → balanced, numbers changed</td><td class="n good">0</td></tr>
|
||
<tr><td>Still out of balance, but closer</td><td class="n">5</td></tr>
|
||
<tr class="total"><td>Days touched · dollars moved</td><td class="n">984 · $62,069.64</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="callout">
|
||
<span class="h">Neither fix touches a healthy day — now confirmed over three times the window</span>
|
||
<p>Across 8,733 client-days and 90 days of trading, not one day that balanced under production's calculation was altered by either fix: zero knocked out of balance, and zero whose amounts moved while staying balanced. Every day they touched was already out of balance. The thirty-day run found the same thing; this replicates it on a much larger sample.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>What actually changed on the page</h2>
|
||
<div class="measure">
|
||
<p>Both fixes add a single credit line. Nothing else in a summary moves — no sales figure, no
|
||
tender, no tax. That is why they can only ever help a day that was already short, and it is
|
||
visible in the line items.</p>
|
||
</div>
|
||
|
||
<h3>R1 — NGLK, 2026-08-04</h3>
|
||
<div class="scroll">
|
||
<table>
|
||
<thead><tr><th>Line</th><th class="n">Before</th><th class="n">After</th><th class="n">Change</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><strong>Tip</strong> (credit)</td><td class="n">482.94</td><td class="n">422.94</td><td class="n bad">−60.00</td></tr>
|
||
<tr><td class="dim">Card Refunds (credit)</td><td class="n dim">60.00</td><td class="n dim">60.00</td><td class="n dim">—</td></tr>
|
||
<tr><td class="dim">every other line</td><td class="n dim" colspan="2">unchanged</td><td class="n dim">—</td></tr>
|
||
<tr><td>Total debits</td><td class="n">10,094.81</td><td class="n">10,094.81</td><td class="n dim">—</td></tr>
|
||
<tr><td>Total credits</td><td class="n">10,154.81</td><td class="n">10,094.81</td><td class="n">−60.00</td></tr>
|
||
<tr class="total"><td>Imbalance</td><td class="n bad">−60.00</td><td class="n good">0.00</td><td class="n">balanced</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="measure">
|
||
<p>Read the two credit lines together and the story is complete: the day already carried a
|
||
<strong>$60.00 card refund</strong> — the guest was given their money back, tip included — but the
|
||
tip was still credited in full at $482.94, because the reversal lives on an order with no tender
|
||
and <code>get-tip</code> only reached tips through tenders. The books claimed $60 of tip income
|
||
that had been handed back. The corrected figure, $422.94, matches the refund exactly.</p>
|
||
</div>
|
||
|
||
<h3>R2 — NTPT, 2026-08-06</h3>
|
||
<div class="scroll">
|
||
<table>
|
||
<thead><tr><th>Line</th><th class="n">Before</th><th class="n">After</th><th class="n">Change</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><strong>Service Charges</strong> (credit)</td><td class="n bad">absent</td><td class="n">427.10</td><td class="n good">+427.10</td></tr>
|
||
<tr><td class="dim">Card Payments (debit)</td><td class="n dim">4,975.89</td><td class="n dim">4,975.89</td><td class="n dim">—</td></tr>
|
||
<tr><td class="dim">every other line</td><td class="n dim" colspan="2">unchanged</td><td class="n dim">—</td></tr>
|
||
<tr><td>Total debits</td><td class="n">7,777.20</td><td class="n">7,777.20</td><td class="n dim">—</td></tr>
|
||
<tr><td>Total credits</td><td class="n">7,350.10</td><td class="n">7,777.20</td><td class="n">+427.10</td></tr>
|
||
<tr class="total"><td>Imbalance</td><td class="n bad">+427.10</td><td class="n good">0.00</td><td class="n">balanced</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="measure">
|
||
<p>The customer paid $427.10 of service charge as part of a $4,975.89 card tender, so the money
|
||
arrived on the debit side. No line credited it, so the day showed $427.10 more collected than
|
||
earned. Adding the credit closes it exactly, and the single order responsible is
|
||
<span class="mono">square/order/NTPT-PT-KrMZzcon1cpQEJUyetErkBIpcdEZY</span>, carrying a
|
||
<code>:sales-order/service-charge</code> of 427.10 on a 3,198.78 order.</p>
|
||
</div>
|
||
|
||
<h3>The largest tip repairs</h3>
|
||
<div class="scroll">
|
||
<table>
|
||
<thead><tr><th>Client</th><th>Date</th><th class="n">Tip before</th><th class="n">Tip after</th><th class="n">Imbalance before</th><th class="n">After</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">NGFL</td><td>2026-05-19</td><td class="n">238.46</td><td class="n">70.42</td><td class="n bad">−168.04</td><td class="n good">0.00</td></tr>
|
||
<tr><td class="mono">NGMI</td><td>2026-07-09</td><td class="n">230.01</td><td class="n">80.01</td><td class="n bad">−150.00</td><td class="n good">0.00</td></tr>
|
||
<tr><td class="mono">NGVA</td><td>2026-07-03</td><td class="n">152.66</td><td class="n">40.12</td><td class="n bad">−112.54</td><td class="n good">0.00</td></tr>
|
||
<tr><td class="mono">NGLK</td><td>2026-08-04</td><td class="n">482.94</td><td class="n">422.94</td><td class="n bad">−60.00</td><td class="n good">0.00</td></tr>
|
||
<tr><td class="mono">NGPA</td><td>2026-06-15</td><td class="n">383.70</td><td class="n">331.19</td><td class="n bad">−52.51</td><td class="n good">0.00</td></tr>
|
||
<tr><td class="mono">NGLK</td><td>2026-06-09</td><td class="n">518.88</td><td class="n">467.69</td><td class="n bad">−51.19</td><td class="n good">0.00</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="measure">
|
||
<p>Every one overstated tip income, and in every case the correction equals the imbalance to the
|
||
cent. NGFL was crediting $238.46 of tips on a day where $168.04 had been given back.</p>
|
||
</div>
|
||
|
||
<h3>The largest service-charge repairs</h3>
|
||
<div class="scroll">
|
||
<table>
|
||
<thead><tr><th>Client</th><th>Date</th><th class="n">Service charges before</th><th class="n">After</th><th class="n">Imbalance before</th><th class="n">After</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">NGPA</td><td>2026-06-04</td><td class="n bad">absent</td><td class="n">1,344.86</td><td class="n bad">+1,344.86</td><td class="n good">0.00</td></tr>
|
||
<tr><td class="mono">NTPT</td><td>2026-08-06</td><td class="n bad">absent</td><td class="n">427.10</td><td class="n bad">+427.10</td><td class="n good">0.00</td></tr>
|
||
<tr><td class="mono">N-30003</td><td>2026-05-27</td><td class="n bad">absent</td><td class="n">405.83</td><td class="n bad">+405.83</td><td class="n good">0.00</td></tr>
|
||
<tr><td class="mono">NGPA</td><td>2026-07-02</td><td class="n bad">absent</td><td class="n">348.45</td><td class="n bad">+348.45</td><td class="n good">0.00</td></tr>
|
||
<tr><td class="mono">NGA1</td><td>2026-06-04</td><td class="n bad">absent</td><td class="n">296.35</td><td class="n bad">+296.35</td><td class="n good">0.00</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="measure">
|
||
<p>NGPA 2026-06-04 is the single largest repair in the ninety days: $1,344.86 of service charges
|
||
collected from customers and credited to no revenue account at all.</p>
|
||
</div>
|
||
|
||
<h3>Where both fixes land on one day</h3>
|
||
<div class="scroll">
|
||
<table>
|
||
<thead><tr><th>NGNP, 2026-06-25</th><th class="n">Before</th><th class="n">After</th><th class="n">Change</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Tip (credit)</td><td class="n">93.90</td><td class="n">92.10</td><td class="n">−1.80</td></tr>
|
||
<tr><td>Service Charges (credit)</td><td class="n bad">absent</td><td class="n">301.40</td><td class="n">+301.40</td></tr>
|
||
<tr class="total"><td>Imbalance</td><td class="n bad">+299.60</td><td class="n good">0.00</td><td class="n">301.40 − 1.80</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="measure">
|
||
<p>The two corrections pull in opposite directions and still land on zero, which is a useful
|
||
check that they are independent and neither is compensating for the other.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>What is left</h2>
|
||
<div class="measure">
|
||
<p>Fifteen client-days above the ten-cent materiality threshold, out of 8,350. Everything else — 90 client-days — totals <strong>$2.26</strong>.</p>
|
||
</div>
|
||
<div class="scroll">
|
||
<table>
|
||
<thead><tr><th>Client</th><th>Date</th><th class="n">Variance</th><th>Note</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">NGBK</td><td>2026-08-06</td><td class="n">+299.42</td><td>tender exceeds order totals by exactly this much</td></tr>
|
||
<tr><td class="mono">NGMV</td><td>2026-05-24</td><td class="n">+100.87</td><td rowspan="5" class="dim">a five-day cluster in late May, new to this window and not yet diagnosed</td></tr>
|
||
<tr><td class="mono">NGMV</td><td>2026-05-20</td><td class="n">+72.05</td></tr>
|
||
<tr><td class="mono">NGMV</td><td>2026-05-21</td><td class="n">+57.64</td></tr>
|
||
<tr><td class="mono">NGMV</td><td>2026-05-22</td><td class="n">+14.41</td></tr>
|
||
<tr><td class="mono">NGMV</td><td>2026-05-26</td><td class="n">+14.41</td></tr>
|
||
<tr><td class="mono">NGEB</td><td>2026-05-19</td><td class="n">−79.01</td><td rowspan="4" class="dim">ezCater fee semantics — the plan's open question §15.4</td></tr>
|
||
<tr><td class="mono">NGEB</td><td>2026-06-23</td><td class="n">−50.28</td></tr>
|
||
<tr><td class="mono">NGEB</td><td>2026-05-13</td><td class="n">−49.80</td></tr>
|
||
<tr><td class="mono">NGEB</td><td>2026-07-29</td><td class="n">−20.00</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">N-30012</td><td>2026-05-20</td><td class="n">+17.32</td><td rowspan="2" class="dim">two consecutive days, undiagnosed</td></tr>
|
||
<tr><td class="mono">N-30012</td><td>2026-05-21</td><td class="n">+12.99</td></tr>
|
||
<tr><td class="mono">NG4S</td><td>2026-05-29</td><td class="n">−11.78</td><td class="dim">undiagnosed</td></tr>
|
||
<tr><td class="mono">PNSP</td><td>2026-07-12</td><td class="n">−0.14</td><td class="dim">register rounding, just over threshold</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="measure">
|
||
<p>Widening from thirty days to ninety surfaced two clusters the shorter window could not see: <strong>NGMV</strong> across five days in late May, and <strong>NGEB</strong> across four days spanning May to July. NGEB is the known ezCater fee-semantics question. NGMV and N-30012 are new and worth a look before this ships — they are the kind of thing only a longer baseline exposes.</p>
|
||
<p>The ten deactivated twins, excluded above, contribute a further $1,142.98 across three days. They should not be reported on at all once one client per location is settled.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Two performance defects found on the way</h2>
|
||
<div class="measure">
|
||
<p>Running the job 56,700 times surfaced two problems that a normal nightly pass would hide, both now fixed or documented.</p>
|
||
</div>
|
||
<div class="scroll">
|
||
<table>
|
||
<thead><tr><th>Problem</th><th>Effect</th><th>Fix</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>dirty-sales-summaries</code> scanned the whole index</td><td class="n">1,321 ms → 5.6 ms per client</td><td>bound the scan at the client boundary</td></tr>
|
||
<tr><td>Transactor sized for a toy database</td><td class="n">~35/min → 5,300/min</td><td>object cache 2 GB → 8 GB, heap 4 GB → 16 GB</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="measure">
|
||
<p>The second is a deployment setting rather than a code change, but it is dramatic: with a 2 GB object cache against a 27 GB database, the final 7,958 client-days of a pass were crawling at about 35 a minute. After resizing they completed in 90 seconds. Worth checking what production's transactor is sized at.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Reproduce it</h2>
|
||
<div class="scroll">
|
||
<pre><span class="dim">;; connect to the restored backup</span>
|
||
(def conn (d/connect "datomic:dev://localhost:4337/integreat-prod-restore"))
|
||
|
||
<span class="dim">;; baseline mode: clear the flag, restore tendered-only tips</span>
|
||
(doseq [[c _] clients]
|
||
@(d/transact conn [[:db/retract c :client/feature-flags "summary-service-charges"]]))
|
||
(alter-var-root #'ss/get-tip (constantly baseline-get-tip))
|
||
|
||
<span class="dim">;; mark the window and run the real job, then capture</span>
|
||
(doseq [[c _] clients] (ss/mark-dirty c w90-start w90-end))
|
||
(pmap (fn [[c code]] (ss/refresh-client! c code)) pending)
|
||
|
||
<span class="dim">;; the single orders behind the worked examples</span>
|
||
(d/pull (d/db conn) '[*]
|
||
[:sales-order/external-id
|
||
"square/order/NGLK-SM-OxSX9gpXJV394qqT8mnBGypUwKNZY"])
|
||
(d/pull (d/db conn) '[*]
|
||
[:sales-order/external-id
|
||
"square/order/NTPT-PT-KrMZzcon1cpQEJUyetErkBIpcdEZY"])</pre>
|
||
</div>
|
||
<div class="measure">
|
||
<p>The unit tests covering both fixes and the re-key:</p>
|
||
</div>
|
||
<div class="scroll">
|
||
<pre>lein test auto-ap.jobs.sales-summaries-test auto-ap.square.core3-test
|
||
<span class="dim">;; => 15 tests, 26 assertions, 0 failures</span></pre>
|
||
</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>
|
||
|
||
<section>
|
||
<h2>Isolation: which entities could still swap owners</h2>
|
||
<div class="measure">
|
||
<p>Balancing was only half the problem. The other half is whether an entity can change hands
|
||
between two clients at all. Rather than reason about it, every entity type the importers
|
||
create with a unique external id was audited, and ownership was read out of the database's
|
||
own history.</p>
|
||
</div>
|
||
|
||
<h3>Entities whose client has actually changed</h3>
|
||
<div class="scroll">
|
||
<table>
|
||
<thead><tr><th>Entity</th><th class="n">Swapped</th><th class="n">Of total</th><th>Key before</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Expected deposits (Square payouts)</td><td class="n bad">4,069</td><td class="n">144,688</td><td class="mono">square/payout/<id></td></tr>
|
||
<tr><td>Cash drawer shifts</td><td class="n bad">2,628</td><td class="n">69,291</td><td class="mono">square/cash-drawer-shift/<id></td></tr>
|
||
<tr><td>Refunds</td><td class="n">3,387</td><td class="n">51,990</td><td class="dim">fixed earlier</td></tr>
|
||
<tr><td>Sales orders, ezCater orders</td><td class="n good">0</td><td class="n dim">—</td><td class="dim">already scoped</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="measure">
|
||
<p>Both payout and cash-shift endpoints are filtered by location, so two clients configured on
|
||
one Square location import the same records and collide on a single entity — the identical
|
||
defect refunds and charges had, in two types nobody had looked at.</p>
|
||
</div>
|
||
|
||
<div class="callout warn">
|
||
<span class="h">Nineteen client pairs have contended, but only ten are visible today</span>
|
||
<p>Nine pairs no longer share a location in the current configuration, so no point-in-time
|
||
check would find them — yet their data is still mixed. The largest are NGMJ/NGSC with 1,546
|
||
affected entities, NGAK/NGMH with 952, NGBW/NGWD with 780 and NGNP/NGVZ with 645. This is the
|
||
argument for scoping the keys rather than only deactivating what currently looks shared:
|
||
deactivation is a snapshot fix that rots the next time someone configures a location twice.</p>
|
||
<p>Worth knowing alongside it: <strong>105 of the clients share a single Square auth token</strong>,
|
||
so these importers all reach into one Square account. The blast radius of a future
|
||
misconfiguration is the estate, not a pair.</p>
|
||
</div>
|
||
|
||
<h3>What was done, and what it cost the books</h3>
|
||
<div class="scroll">
|
||
<table>
|
||
<thead><tr><th>Step</th><th>Result</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Re-key expected deposits</td><td class="n">144,652 re-keyed · 0 collisions · 36 unscopable</td></tr>
|
||
<tr><td>Re-key cash drawer shifts</td><td class="n">69,291 re-keyed · 0 collisions · 0 unscopable</td></tr>
|
||
<tr><td><strong>Entity counts before vs after</strong></td><td class="n good">unchanged on all four types</td></tr>
|
||
<tr><td>Legacy-scheme keys remaining</td><td class="n good">0</td></tr>
|
||
<tr><td>Live re-import, 66 of 102 clients, 90-day window</td><td class="n good">0 entities created · 0 values rewritten</td></tr>
|
||
<tr><td><strong>Ownership changes since the re-key</strong></td><td class="n good">0 deposits · 0 shifts · 0 refunds</td></tr>
|
||
<tr><td><strong>Summaries recomputed and compared</strong></td><td class="n good">12,044 · 0 differences</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="measure">
|
||
<p>The re-import is the important negative result. Every payout and shift the importer fetched
|
||
resolved to the client's own entity and wrote nothing — the re-key is transparent to the
|
||
importer, which is exactly what the legacy-key fallback is for. And recomputing 12,044
|
||
client-days afterwards reproduced the previous figures to the cent, so re-keying 213,943
|
||
entities moved no money at all.</p>
|
||
<p>One caveat worth stating plainly: re-keying stops future contention, but it does not
|
||
retrospectively re-attribute records that were claimed by the wrong client while the
|
||
configuration was shared. Those stay where they were last written. Correcting them is a
|
||
separate exercise, and one to do only once the business decides which client owns each
|
||
location.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Why <code>remove-voided-orders</code> is dangerous</h2>
|
||
<div class="measure">
|
||
<p>The schema says a charge belongs to its order:</p>
|
||
</div>
|
||
<div class="scroll">
|
||
<pre>{:db/ident :sales-order/charges, :db/isComponent true}</pre>
|
||
</div>
|
||
<div class="measure">
|
||
<p><code>:db/isComponent</code> means Datomic treats charges as <em>parts of</em> the order rather
|
||
than independent records. So <code>[:db/retractEntity <order>]</code> deletes the order
|
||
<strong>and its charges</strong>. That is the documented behaviour, and normally it is what you want.</p>
|
||
<p><code>remove-voided-orders</code> asks Square for the last ten days of orders, keeps the ones
|
||
Square reports as voided, and retracts them. Correct on its own terms.</p>
|
||
<p>The problem is that where two clients shared a location, both clients' orders resolved to the
|
||
<strong>same charge entity</strong>, because charge keys carried no client:</p>
|
||
</div>
|
||
<div class="scroll">
|
||
<pre>NGCD order ──┐
|
||
├──> charge 17592490524 ← one entity, two parents
|
||
NGCC order ──┘</pre>
|
||
</div>
|
||
<div class="measure">
|
||
<p>Retract either order and Datomic deletes that shared charge. The other client's order survives
|
||
with its sales intact but its payment gone: the day silently goes out of balance and the tender
|
||
disappears from the current database value. Measured on the restore,
|
||
<strong>35,870 of 56,829 charges (63%)</strong> in the contended clients' recent window have two
|
||
parent orders.</p>
|
||
<p>The re-key stops new sharing but does not un-share those. Two remedies, either sufficient:
|
||
split them by re-importing the affected window now that keys are client-scoped, or guard the
|
||
retraction so it detaches a shared charge instead of deleting it. The guard is small and makes
|
||
the operation safe whatever shape the data is in.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<footer>
|
||
<span>Measured 2026-08-15 on <span class="mono">integreat-prod-restore</span>, restored from backup point 209608347. Nothing in production was touched. 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>
|