Hack #022 · Billing

A statement total froze while its PDF kept recalculating lines

How to make a statement's total and its line items come from one frozen snapshot, so a later price change can't leave a paid statement short.

2026-09-14 · 6 min · we hit this one ourselves

By Chen Deng · OxOne, Calgary AB

Do this

Stop rendering statements from a stored total plus live line-item queries — snapshot every line at issue time and sum the total from those saved rows so both always agree.

A client's monthly statement carried one number in the header — a subtotal saved when the statement was generated. The PDF underneath it didn't store the lines; it re-queried the product and order tables every time someone opened or re-downloaded the file. So the line items were always current. The header total was always historical. As long as nobody touched a price, the two matched and nothing looked wrong.

Then a price got corrected weeks after a statement went out. The PDF's line items quietly recalculated to the new price. The frozen header total didn't move. The customer had already paid the old, lower total — so the statement was short, the PDF showing higher line amounts than the total it was paid against. Because both numbers were internally plausible, nobody caught it for months.

The break is two sources of truth

The problem isn't the total or the lines individually. It's that they come from different moments in time on the same document.

statement generatedtotal saved as a columna price edited laterPDF re-reads live linestotal no longer matches

A financial statement is meant to be a record of what was owed on the day it was issued. The moment any part of it reads live data, it stops being a record and becomes a live report that happens to have a date printed on it.

Snapshot the lines, then sum the total from them

The fix is to make the document read entirely from frozen data, and to derive the total by adding up those frozen lines — not from a separately stored number that can drift away from them.

✗ What broke it

Header total is a saved scalar. PDF template loops over a live query of orders/products at render time. The two are never checked against each other.

✓ What holds

At generation, copy each line — description, quantity, unit price, line total, tax — into statement-line rows. The PDF renders only those rows, and the total is SUM(line_total) of them.

Do this in order:

  1. Open the PDF template and find where line items come from. If you see a query against your live products, orders, or pricing tables, that's the leak.
  2. Check the total. If it's a stored column set once at generation, note it — it's about to become a computed value instead.
  3. Add statement-line rows captured at issue time: description, qty, unit price, line total, and the tax charged on each line.
  4. Rewrite the template so it reads only those saved rows, and print the total as the sum of them. Now the total literally cannot disagree with the lines.
  5. Lock issued statements against edits. Once a statement is sent, price changes create the next statement or a credit note — they never reach back into a sent one.
  6. Reconcile the back catalogue: re-open past statements, compare the printed total to the sum of current line items, and flag any that differ.
GST drifts too

In Alberta there's 5% GST and no provincial sales tax, so tax is a clean per-line calculation — which means a recalculating line silently recalculates the GST on it. Snapshot the tax amount per line, not just the pre-tax price, or your CRA remittance figure won't match what the customer actually paid.

A sent statement should be a photograph, not a live feed.

Where this stops being a DIY fix

Rewriting a template to read snapshot rows is doable in an afternoon. The reconciliation is the hard part: if you find historical statements that were paid short, you're into re-issuing or crediting real money, and if GST was under-remitted you may have filings to correct. That's an accountant conversation, not a code one. And if your billing runs on an off-the-shelf platform where you can't see how the PDF is built, you can't fix the snapshot yourself — you can only test it, then take the evidence to the vendor.

Test it the same way every time: issue a statement, change a price on one of its products, re-download the PDF. If any number on the paper moved, the document is still reading live.

Every one of these came out of work we actually did. If you would rather not do it yourself, book a free 30-minute diagnostic and we will tell you which of these your business is losing money to.

More on this Business automation for Calgary companies Everything we have written