Three faults were leaving restaurant days out of balance — one in the
data, two in the arithmetic — plus a fourth that turned out to be a
missing-data problem and is deliberately left visible. Measured over ninety days on a restored
copy of production (210 clients, 18,900 client-days): 1,258 days out of
balance and $69,560.10 becomes 279 days and $7,790.54 — of which 171 are
not arithmetic faults at all, but days whose sales were never imported.
979 days repaired, none knocked out of balance, and not one
already-balanced day altered — verified line by line (category, side,
amount to the cent, account), not just on each day's bottom line.
THE DATA FAULT
Ten Square locations were configured against two client records each.
Sales orders scoped their identifier by client; refunds, card payments,
payouts and cash-drawer shifts used the bare Square id. Those attributes
are :db.unique/identity, so both clients' imports resolved to a single
entity and the last writer won — 3,387 refunds, 4,069 payouts and 2,628
cash-drawer shifts changed hands over time, across 19 client pairs of
which only 10 are visible in today's configuration.
Worse, one payment could belong to two orders. :sales-order/charges is
:db/isComponent, so removing a voided order cascaded into payments the
other client still needed.
Fixes: client-scope the four key schemes; look the record up under both
schemes so the change deploys before the migration finishes; and a
migration that gives every order its own payment. Run over the whole
database that is 19,040,785 orders walked, 9,100,314 payments re-keyed
and 200,027 copied, ending with 17,047,142 payments scoped, none left to
rename, none unscopable, and no payment owned by more than one order.
Idempotent and resumable; about thirteen minutes.
THE ARITHMETIC FAULTS
- Refunded tips stayed on the books. get-tip summed tips by joining
through :sales-order/charges, so a return-only order — no tender to
join through — contributed nothing while its reversal sat unread on
:sales-order/tip. Additive, not substitutive: where an order does have
a tender the tender is the correct source.
- Service charges were collected but never earned. Nothing read
:sales-order/service-charge. Now credited for Square orders only, both
signs, behind summary-service-charges.
The flag is off by default, so deploying this changes nothing until a
client is opted in. docs/2026-08-15-sales-summary-rollout-plan.md has the
steps.
WHAT IS DELIBERATELY NOT FIXED
156 of the 279 remaining days carry refunds on a record that recorded no
sales at all that day, and 132 of those fall before that client's first
ever order. The refunds are not theirs: ownership history shows a $35.35
refund dated 26 February belonging to NGDG that day and taken over by
NGDU on 12 August, flipping between the two several times a day. Across
nine records, 659 refunds worth $15,225.24 sit on a record dated before
its own first order — unscoped keys let whichever import ran last take
ownership.
A rule closing those days was written and measured (156 days, $4,820.19,
nothing broken) and then removed. An unbalanced day is the only visible
signal that a restaurant's sales are not being imported; balancing it
would remove the alarm and leave the fire. A comment and a test hold that
decision in place. Step 9 of the rollout plan is the real fix, and it
needs a business decision.
SUPPORTING
- Install schema attributes before the tuples that compose them. A tuple
in schema.edn is built from an attribute in cloud-migration-schema.edn,
so every test fixture died in setup — very likely why sales summaries
had no tests before this.
- Log each day's imbalance and its suspect lines.
- Bound the dirty-summary scan to one client: 1,321 ms to 5.6 ms.
- compare-sales-summaries lives in test/clj as auto-ap.tools.* — it is a
verification harness, not part of the running application. Its
docstring now warns that d/as-of cannot be used to compare summary
amounts: :ledger-mapped/amount, ledger-side and account are
:db/noHistory, so a recomputed summary reads back with its amounts
absent and looks like a legitimate balanced day.
26 tests, 62 assertions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sales-summaries-v2 recomputes every dirty summary, but nothing set the dirty
flag on a schedule: mark-all-dirty was only ever called by hand from the
comment block, and the job was registered in neither server.clj's
INTEGREAT_JOB dispatch nor terraform/deploy.tf, so -main was dead code that
could never run in production. Summaries were only recalculated when someone
remembered to do it in the REPL, and POS data keeps arriving after a business
day closes, so a summary computed once on the day was routinely wrong and
stayed wrong.
Add a daily job that marks the trailing 7 days dirty and recomputes them,
leaving finished work alone. "Finished" is the condition the app already calls
Balanced -- debits equal credits and every line is mapped to an account. Since
that is derived rather than stored, a summary that later falls out of balance
is picked up again on the next run.
Extract the Balanced predicate into auto-ap.datomic.sales-summaries so the
grid's pill and the job share one definition, rather than a background job
requiring an SSR namespace. total-debits/total-credits resolve the ledger side
from either a plain keyword or the {:db/ident ...} map a pull returns, and
accepted? requires every item to declare a side: un-normalized pulled items
otherwise sum to 0.0 on both sides, read as balanced, and get skipped
silently and permanently.
Also fix sales-summaries-v2 destroying user-entered line items. It filtered
for :sales-summary-item/manual? to preserve them, but dirty-sales-summaries'
index-pull selector never fetched :sales-summary/items, so manual-items was
always empty. Because items is a component attribute upserted via
[:reset-rels ...], every recompute deleted the hand-entered lines -- often the
very lines that make a summary balance. Harmless while nothing ran on a
schedule; destructive the moment this does.
Register the job in the admin Background Jobs dropdown too, with a days
field: schedules are prod-only, so the admin page is the only way to run it
on staging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## Summary
Completes the automatic sales summary pipeline end-to-end: the `sales-summaries-v2` job now calculates aggregate totals, preserves manual adjustments, and automatically posts balanced journal entries to the ledger.
## What Changed
**New Datomic transaction function** (`upsert-sales-summary-ledger`)
- Transforms detailed `sales-summary-item`s into aggregated `journal-entry` lines grouped by account and ledger side
- Handles the full upsert: posts a new journal entry for summaries with mapped accounts, or retracts the orphaned entry if items no longer qualify
**Enhanced `sales-summaries-v2` job**
- Calculates and stores 13 aggregate total attributes (card/cash/food-app/gift-card payments, refunds, fees, discounts, tax, tip, returns, unknown, net)
- Preserves manual items (`manual? true`) during recalculation — only auto-calculated items are replaced
**Ledger reconciliation**
- `reconcile-ledger` now queries for sales summaries missing journal entries and repairs them via `:upsert-sales-summary-ledger`, alongside existing invoice and transaction repairs
**Schema**
- Added 13 `total-*` attributes on `sales-summary` (all `db.type/double`, no history)
- Registered the new transaction function in `tx.clj` and `datomic.clj`
**Admin UI cleanup**
- Resolved "clientize" and HTMX `client-id` TODOs in the sales summaries admin page
- `new-summary-item` now correctly passes `client-id` via `hx-vals`
- Removed stale TODO comments and placeholder code
## Files Changed (8)
| File | Purpose |
|------|---------|
| `iol_ion/.../upsert_sales_summary_ledger.clj` | New Datomic tx function |
| `iol_ion/.../tx.clj` | Register new tx function |
| `resources/schema.edn` | 13 new `total-*` attributes |
| `src/.../datomic.clj` | Load new tx namespace |
| `src/.../jobs/sales_summaries.clj` | Aggregate totals + manual item preservation |
| `src/.../ledger.clj` | Sales summary repair in `reconcile-ledger` |
| `src/.../ssr/admin/sales_summaries.clj` | UI TODO cleanup |
| `docs/plans/...plan.md` | Implementation plan document |
Co-authored-by: Bryce <bryce@integreatconsult.com>
Reviewed-on: #5
Co-authored-by: Bryce <bryce@brycecovertoperations.com>
Co-committed-by: Bryce <bryce@brycecovertoperations.com>