Skip to content
VC
Case 15 of 33 · E-commerce · Analytics

Revenue reconciliation across three sources: an 8.4% gap explained down to 0.33%

A US outdoor-gear brand had three sources using one word, revenue, for three different numbers: the owner's operational book, the merchandiser's warehouse book and the in-house CRM. A one-off three-way reconciliation explained the site gap and caught a broken year-over-year comparison. The check then became a daily job with a 3% threshold and one rule: below 70% coverage, no verdict is issued.

Industry
E-commerce · two Shopify stores
Stack
Python · Shopify Admin API · Google Sheets API · systemd
Timeline
One-off reconciliation — 1 day, daily pipeline — 1 week
Outcome
8.4% gap → 0.33% remainder
01 · Pain Point

Three reports, three revenues, and an argument over which to trust

The owner keeps a daily sales sheet in Google Sheets: two stores, two marketplaces, spend by channel. The merchandiser keeps the warehouse book: one row per order line, purchase price, logistics, a hand-marked sales channel. The CRM computes straight from Shopify. Over the same 19 days the three sources showed three different site revenues, and the gap between the owner's book and the CRM came to 8.4%.

The owner asked the question verbatim: why do these numbers disagree with Shopify? Until that is answered, every budget decision runs into an argument about figures: each side has its own report, and each is right inside its own definitions.

The same sheet carried a year-over-year comparison: −12.84% on total revenue. It was being read as the business shrinking.

02 · Solution

One code path, one window, every gap traced to an order

The work ran in two stages. First, a one-off three-way reconciliation that settled the argument and gave the owner a written summary. Then the same check became a permanent job with a snapshot and a threshold, and the fresh code went through an adversarial audit.

01
Same code

Shopify is queried for the same window with the same functions the CRM uses: the check and the report share one arithmetic

02
Line by line

Every gap is traced to specific orders: cancellations, partial refunds, the day boundary

03
Third source

The merchandiser's warehouse book is matched against the CRM per sales source: site, two marketplaces

04
Year over year on equal windows

19 dates of this year against the same 19 dates of last year, with no full month in the denominator

05
Summary for the owner

Six sections; what came from each side's reports and what was verified against primary data are marked separately

Two quantities under one word

The 8.4% site gap consisted exactly of cancelled and partially refunded orders. The owner's book records the amount at the moment of ordering; the CRM shows the money left after cancellations and refunds. Both figures are correct and answer different questions: how much was sold, and how much money stayed. The 0.33% remainder sits on orders at the day boundary. A gap in the CRM itself was closed along the way: the rule "a cancelled or fully refunded order is no revenue" worked, while a partially refunded order counted in full — 7 orders in 30 days, 0.47% of the total. The refund is now subtracted and printed as a separate line, so managers who see the original order amount in their own table get an explanation.

The warehouse book: reconciliation became a daily job

On day one the warehouse book was matched with the CRM by hand, into a file in a temporary folder, and by the next day the check was gone. The reconciliation module makes it permanent: it reads the book through a range export, computes revenue and cost per sales source, compares line by line with Shopify using the same code the CRM runs, and writes a snapshot to a file. CRM pages read the snapshot: reading the book directly would cost a 16-thousand-row export on every page open. The job runs on a daily timer. The 3% threshold, against a measured 0.6%, leaves room for the day boundary and manual edits. A discrepancy ends the run normally and prints the difference; the service fails in exactly one case — when the reconciliation did not happen — and an alert goes to the team chat.

The books are the merchandiser's working tool, so the module is read-only: two tests read the module's source text and confirm it contains no write function.

Four rounds of root-cause analysis

The first live run put the site total at +0.62% and painted the per-store lines red. Each cause had to be found on matched orders:

  • the book stores the price before discount, Shopify stores the amount paid; the book is now compared with "paid plus discount", and the match was verified order by order
  • some orders carry a row with no price in the book — free accessories; such an order is removed from the comparison on both sides, and the size of the gap is written to the snapshot as a separate field
  • "manual" orders are a CRM concept; in the book they sit under the regular store, so the comparison folds them back into their store
  • the book writes one store's order numbers with a leading zero; before normalization 100 orders out of 158 looked missing, after it 153 of 158 matched

After four rounds the check settled: −0.81% on one store, −0.12% and 0.00% on the marketplaces, against a 3% threshold. On one marketplace all 62 orders out of 62 matched, none lost on either side.

An adversarial audit of day-old code

A few days later independent reviewer agents went through the module with one brief: find what is wrong. The tally — 68 findings, 42 confirmed, 11 defects in code written the previous day. Each was reproduced before the fix and closed with a test, and each test was checked by putting the bug back: the test goes red.

  • the window was requested in UTC and filtered by the store's local date: 154 orders out of 959 fell on the day boundary, 15% of the money; a one-day window showed a 15% gap against a perfectly good book. The window is now fetched with a day of margin on both sides and filtered by the requested dates
  • a write-off line for defective stock went into sales because of an exact string match, turning a 50.0% margin into 59.3%; the category is now read by prefix
  • the sales source was matched case-sensitively: one lowercase letter in a column filled in by hand produced "mismatch" against a healthy book
  • the book's line price was compared with an order total that includes tax and shipping; the comparison now uses the subtotal
  • the module answered "matched" when its own side failed completely: the book had money, Shopify did not respond, the list of mismatches was empty. "Matched" now requires at least one verified line and no unverified ones
  • the verdict ignored coverage: the audit's synthetic data — 101 orders, 100 of them with an unpriced row — produced "matched" on 0.25% of the money. Coverage is now printed in the snapshot, and below 70% no verdict is issued
  • the snapshot printed "money outside the comparison" as the value of affected orders — five times the real sum of differences; that is now three separate fields
  • margin was computed on mixed sets of rows; it now uses only rows where both the price and the purchase cost were read

The module now has 33 tests: 15 on launch day, 24 after the root-cause rounds. Test values are taken from live book cells.

The snapshot made it to the screen

The audit also flagged an organizational defect: the snapshot went into a file nobody opened. A signal nobody sees protects nothing. A "Warehouse books" card on the CRM integrations page now shows, per line, the book amount, the Shopify amount, the difference and the verdict, with comparison coverage and purchase-price margin alongside.

03 · Stack

The same code the CRM runs, and read-only access to other people's books

Python

A module in the reporting layer; Shopify revenue is computed by the same functions the CRM uses

Shopify Admin API

Orders of both stores for the window with a day of margin on each side, filtered by the store's local date

Google Sheets API

The warehouse book is read through a range export; two tests confirm the module contains no write function

JSON snapshot

The result lives in a file; pages open it instantly and the book stays untouched

systemd: timer and alert unit

Daily run; the service fails only when the reconciliation did not happen, and the alert reaches the team chat

pytest

33 tests with values taken from live book cells; every fix verified by putting the bug back

Adversarial audit

Independent reviewer agents hunt for defects in fresh code: 68 findings, 42 confirmed

Owner's Google Sheet

Reconciled on the same window; the year-over-year error was caught by recomputing on equal windows

PythonShopify Admin APIGoogle Sheets APIJSONsystemdpytestAdversarial audit
04 · Results

The argument is over, and the check runs every day

Site revenue gap
8.4% 0.33%

the remainder sits on orders at the day boundary

Year over year
−12.84% ≈ +30%

19 dates had been divided by a full month of the previous year

Audit of the reconciliation module
11

defects in day-old code: 68 findings, 42 confirmed

Both figures are correct, and now they have different names

The 8.4% site gap is fully explained: cancelled orders plus partial refunds, with a 0.33% remainder on the day boundary. The warehouse book agreed with the CRM within 0.6%; one marketplace matched exactly, and on the other all three sources agree within 3.5%. One caveat was stated out loud: rows reach the warehouse book from Shopify — by hand and through our own orders-to-warehouse-sheets pipeline — so the 0.6% match confirms the accuracy of the transfer: no order lost, no amount doubled. The book's independent part is purchase price and logistics, which exist nowhere else.

The −12.84% decline turned out to be growth of about +30%

In the sheet, this year's August row summed 19 dates while last year's row held the full month, so 19 days were divided by 31. On equal windows, 1–19 August of both years, site revenue grew 29%, and 30% with the marketplaces included. The owner received a six-section summary that marks what came from his own reports and what was verified against primary data. One of my own claims was withdrawn inside that summary: the first version said spend on one channel was missing from the sheet; reading all 37 columns found it, matching the platform's billing within 0.04%. A conclusion drawn from absence requires proof that the reading was complete.

Reconciliation became a daily job

After four rounds of root-cause analysis the strict lines settled: −0.81% / −0.12% / 0.00% against a 3% threshold. A live run a week after launch: 701 rows, differences of +2.18% / −0.02% / +0.00%, verdict "matched". The snapshot opens instantly on the CRM integrations page, together with comparison coverage and purchase-price margin.

A verdict only with enough coverage

The rule that will outlive this project: a measured zero differs from an inability to measure. "Matched" now means no line diverged, at least one line was actually verified, nothing is left unverified, and the comparison covers at least 70% of the money. Below that there is no verdict, and the screen says "not verified". A complete failure on our side reads as a failure, and an empty denominator prints a dash.

The outcome for the owner: the argument over whose numbers are right is closed, every gap is explained down to specific orders, and the check runs daily and can be inspected. End-to-end attribution answers which ads bring orders; this work answers which figures about money to trust.

05 · Where it fits

Where else the same methodology applies

Behind the case is a standard problem: "two or three accounting systems use one word for different numbers". The method transfers anywhere reports are assembled from several sources:

  • A store plus marketplaces — the storefront shows the order amount, the marketplace pays out net of its fee; the two add up only after being brought to one quantity
  • An ERP, a CRM and a bank — revenue by shipment, by payment and by receipt differ by definition, and the argument is settled by a reconciliation on one window
  • Reports assembled by hand in spreadsheets — period comparisons on unequal windows, a partial month over a full one, column sums with no filter on row type
  • Warehouse and procurement books as the only source of cost price — matching them with sales yields the real margin
  • An audit of existing reporting before budget decisions — before a "decline" in a sheet leads to cutting a channel that works
What's reused on subsequent projects
  • Reconciliation with the same code that computes the main report: the report and the check share one arithmetic
  • The coverage rule: a verdict is issued when the comparison covers enough of the money; a measured zero differs from an inability to measure
  • A snapshot file plus a page that shows it; a daily timer; the job fails only when the reconciliation did not happen
  • Read-only access to other people's working books, enforced by tests over the module's source text
  • An adversarial audit of fresh code: reproduce, fix, close with a test, check the test by putting the bug back
  • A client summary that separates what came from each side's reports from what was verified against primary data
Similar challenge?

If three reports give three revenues — it reconciles down to the order

The right starting point is a one-off reconciliation on a single window: in a day it shows where the definitions diverge and where the error sits in the calculation itself. From there the check becomes a scheduled job with a snapshot, a threshold and a coverage rule.

Ready to start?

The 9,900 ₽ audit — with a concrete report and quote

I'll tell you what to deploy in your business first, what the payback looks like, and whether you need AI for the task at all (sometimes you don't).

Or just send your question — I reply within 2 hours