Sales summaries · restored production backup

Thirty-Day Reconciliation

What the deduplication work and the two calculation fixes actually did to the books, measured day by day against production's own summaries.

Window 2026-07-15 → 2026-08-13 Client-days 3,040 with activity Compared against basis-t 209608347
Baseline $22,527.40 398 days out of balance · 86.00% clean
After fixes $405.66 67 days out of balance · 97.64% clean
331client-days brought into balance
0days knocked out of balance
237already-balanced days whose numbers moved
98.2%of the dollar variance removed

Both arms are computed on the same deduplicated data, so this isolates what the calculation 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).

Was anything already balanced disturbed?

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 237 client-days that were balanced had their line amounts change anyway, and a balance-only view would hide every one of them.

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.

Outcome, production → finalClient-daysReading
Out of balance → balanced309the intended repair
Balanced → out of balance0nothing was broken
Balanced → balanced, numbers changed237amounts moved, balance held
Balanced → balanced, identical1,652untouched
Still out of balance44residual, see below
Production summaries in window2,242of 6,300 client-day slots

What moved on those 237 days

PatternDaysCause
Every category 0.00 → real values~100production held an all-zero summary for a day that had orders
Card Payments ↔ Fees reallocation~135the payout arrived, so the processing fee is now known and booked separately
Service Charges line appears13R2, on days that were already out of balance

The second pattern is worth reading carefully: Card Payments falls by exactly what Fees 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. Three real rows:

ClientDateLineBeforeAfter
NGAN2026-08-12Card Payments1,237.231,207.64
Fees0.0029.59
NGVC2026-08-10Card Payments2,025.032,074.63
Fees49.600.00
NGGP2026-08-13every line0.00real figures

NGAN moves $29.59 out of Card Payments and into Fees; NGVC moves $49.60 the other way. Both stay balanced to the cent. NGGP is the first pattern — production held an all-zero summary for a day with $2,478.44 of card payments, $1,148.11 of gyros and pitas, and so on.

Impact by underlying fix

Applied in sequence, each measured against the state before it. Percentages of a 3,040 client-day population.

Data

Deduplication — Phase 0, re-key, re-import
OutcomeClient-days
Unchanged1,581
Numbers changed547
Out of balance → balanced56
Balanced → out of balance19
Balanced → balanced, numbers changed237

The data work is the only step that unbalances anything: 19 days 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.

R1

Tips on untendered orders
OutcomeClient-daysShare
Unchanged2,94196.7%
Out of balance → balanced862.8%
Balanced → out of balance0
Balanced → balanced, numbers changed0
Still out of balance, but closer130.4%
Days touched · dollars moved99$973.79

R2

Square service charges, both signs
OutcomeClient-daysShare
Unchanged2,76090.8%
Out of balance → balanced2769.1%
Balanced → out of balance0
Balanced → balanced, numbers changed0
Still out of balance, but closer40.1%
Days touched · dollars moved280$23,729.45
Both calculation fixes are inert on healthy days

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.

Worked examples

Each fix is easiest to check on a single order. In every case below the day's imbalance equals the fix amount exactly, which is what you would expect if the fix is booking something real that was previously booked nowhere.

R1 — a tip reversal with no tender to hang it on

FieldValue
Ordersquare/order/NGLK-SM-OxSX9gpXJV394qqT8mnBGypUwKNZY
:sales-order/tip−60.00
:sales-order/total−60.00
:sales-order/charges0 — no tender at all
NGLK 2026-08-04 imbalance−60.00 → 0.00

A guest's tip was handed back. The reversal is recorded on the order, but get-tip reaches tips by joining through :sales-order/charges, and a return-only order has no charge to join through — so the −$60.00 was invisible and the day credited a tip that no longer existed. The other four largest R1 repairs are the same shape: NGFF 08-01 (−$40.00), NGSG 07-29 (−$40.00), NGNV 08-09 (−$35.00), NGJS 07-25 (−$35.00).

R2 — a service charge collected but credited nowhere

FieldValue
Ordersquare/order/NTPT-PT-KrMZzcon1cpQEJUyetErkBIpcdEZY
:sales-order/service-charge427.10
:sales-order/total3,198.78
NTPT 2026-08-06 imbalance+427.10 → 0.00

The $427.10 is inside the card tender the customer paid, so it lands on the debit side — but nothing credited it, leaving the day short by exactly that amount. Next largest: NGFO 07-16 ($275.23), NGRN 08-06 ($236.96), N-30008 07-24 ($229.42), N-30003 07-18 ($225.00).

The 19 days the data work unbalanced — and all 19 rescued

ClientDateAfter data fixAfter R1 + R2
NGSF2026-08-13+137.990.00
NGRY2026-08-13+103.260.00
NGDS2026-08-13+100.520.00
NGDV2026-08-13+100.520.00
N-300072026-08-13+86.000.00
N-300032026-08-13+83.530.00
NGSC2026-08-13+75.000.00
NGGH2026-08-13+75.000.00
NGDG2026-08-13+70.000.00
NGDU2026-08-13+70.000.00
All 19 daysunbalancedbalanced

These are days production held an empty summary for — trivially balanced at zero. Filling them in with real figures exposes the same two calculation gaps every other day had, and both fixes then close them. Not one of the 19 survives as a regression. Their clustering on 08-13 is expected: it is the most recent day, so it is the one production had least chance to compute.

Days production never had

Of 6,300 client-day slots in the window, production held a summary for only 2,242. The remaining 4,058 had none at all — nothing to compare against, and nothing an accountant could have looked at.

They are not evenly spread. Eight consecutive days show 210 missing summaries each, which is every client in the database:

DatesMissing per dayReading
Jul 15 – Jul 29117clients with no summary that day
Jul 30 – Aug 6210every client — a total coverage hole
Aug 7 – Aug 1389clients with no summary that day
The coverage hole is real and self-confirming

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.

What is left

Five client-days above the ten-cent materiality threshold, totalling $404.04. Everything else — 62 days — comes to $1.62, with the largest single day at nine cents.

ClientDateVarianceExplanation
NGBK2026-08-06+$299.42tender exceeds order totals by exactly this much, in the source data
NGDA2026-08-01−$50.00auto-gratuity booked as a service charge
NGEB2026-08-10−$25.00ezCater fee semantics predicted
NGEB2026-07-29−$20.00ezCater fee semantics predicted
NGPS2026-08-12+$9.62unexplained predicted

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.

NGBK 2026-08-06, traced. Summing that client-day's orders directly: tender totals $6,358.99 while the orders' own totals come to $6,059.57 — a difference of $299.42, exactly the imbalance. Square recorded more payment than the orders account for. Three explanations were checked and ruled out: the day has no refund lines at all, no order carries two charges for one payment id, and the nine orders lacking line items carry zero tender. This is a source-data discrepancy, not a calculation defect, and not something either fix should paper over.

How this was measured

  • Run against a production backup restored to basis-t 209608347, newest transaction 2026-08-14 22:52. Nothing in production was touched.
  • "Before" is production's own stored summaries, read via as-of — not a re-simulation of them. Datomic keeps every past value, so the comparison is against exactly what was there.
  • 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.
  • R1 and R2 only ever add credits, so baseline is derived as fixed + untendered tip + service charges. That identity was checked against a from-scratch baseline recomputation on 20 random client-days and agreed on every one.
  • All 14,458 summaries in the database were recomputed through the real job, not a test harness.
Two defects still open before this ships

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, remove-voided-orders retracts orders, and 63% of the contended clients' charges have two parents, so a retraction there deletes the other client's payment.

Reproduce it

Every figure here comes from the restored database and can be re-derived from it. The comparison tool is committed as auto-ap.jobs.compare-sales-summaries.

;; connect to the restored backup (read-only is fine)
(def conn (d/connect "datomic:dev://localhost:4337/integreat-prod-restore"))

;; the whole comparison: production's own summaries vs now
(require '[auto-ap.jobs.compare-sales-summaries :as cs])
(def r (cs/compare-against 209608347
                          (clj-time.core/date-time 2026 7 15)
                          (clj-time.core/date-time 2026 8 14)))

(:tally r)                                  ;; identical / numbers-changed / added
(:balance-transitions r)                    ;; the four-way counts
(count (:previously-balanced-and-changed r)) ;; => 237

;; what actually moved on one of those days
(cs/line-diff (first (:previously-balanced-and-changed r)))

;; the single order behind the R1 example
(d/pull (d/db conn) '[*]
        [:sales-order/external-id
         "square/order/NGLK-SM-OxSX9gpXJV394qqT8mnBGypUwKNZY"])

;; the single order behind the R2 example
(d/pull (d/db conn) '[*]
        [:sales-order/external-id
         "square/order/NTPT-PT-KrMZzcon1cpQEJUyetErkBIpcdEZY"])

The unit tests covering both fixes and the re-key run with:

lein test auto-ap.jobs.sales-summaries-test auto-ap.square.core3-test
;; => 15 tests, 26 assertions, 0 failures

Note that the restore currently has Phase 0 applied and the twins deactivated. Re-running the comparison after re-activating them will move the contended clients' figures; everything outside those ten client pairs is unaffected.