Skip to content

Allow this site to count your visits with Yandex Metrica? Metrica will store cookies in your browser to do this. How the site uses cookies

VC
Case 14 of 22 · AI · Sales analytics

A bot that answers with sales figures: six metrics, with the arithmetic checked by AI reviewers

The owner of a US outdoor-gear brand asked his Telegram bot how much they had sold, by country, over two years, and got no figures back. Now the bot answers with live numbers from both Shopify stores: countries, years, top products by revenue and by units, and US states by sales channel. All the calculations go through one shared module, the metrics core. An hour after release, four AI agents reviewed the code and found four errors. Each one changed the meaning of an answer.

Industry
E-commerce · two Shopify stores
Technology
Python · Shopify Admin API · Telegram · pytest
Timeline
Two stages in one week
Outcome
2 → 6 metrics, zero gap with the report
01 · Problem

The bot was connected to Shopify and still answered without figures

The client is a US outdoor-gear brand with two Shopify stores. The owner asks the bot questions in the messenger between other tasks. By then the bot already had a knowledge base built from the team's chats. It also had a small module that pulled live data from the store. That module could calculate two metrics: revenue for a period, and the split between core products and accessories.

In August the owner asked: "how much did we sell in 2025 and 2026, and to which countries besides the US and Canada?" The answer came from the knowledge base, as text with no figures. I checked: the store connection worked, and "revenue for 30 days" came back straight away. There was simply no geography among the metrics. The cause went deeper: the reporting module never asked Shopify for the shipping address, so the data had no country in it, even though Shopify provides one.

A second error turned up the same week. Asked "what are the top products by revenue", the bot replied with the store's total revenue, because the word "revenue" was matched first. The reply sounded confident and answered a different question. An answer like that is dangerous: the owner trusts it and makes decisions based on it.

02 · Solution

Six metrics, one shared calculation: code does the arithmetic

The bot recognises a question by its keywords and matches it to a fixed set of metrics. Code does the calculation, through the same core that builds the official store report. Every figure is calculated by strict rules, and if it can't be calculated, the answer has no figure. A question the bot doesn't recognise goes to the knowledge base, as before.

01
The question

The owner writes to the bot in Russian or English: “top products for 30 days”, “sales by state for 2026”

02
Reading the question

Keywords and a strict order of checks: states before countries, top products before revenue, years before days

03
Collector modules

Separate modules for countries, top products and states by channel. They take the period, statuses and refunds from the core

04
The metrics core

One rule for which orders count, refunds per line item, discounts spread across items, the store's time zone. The same arithmetic for the bot and the report

05
Answer and question log

Figures are shown in money and in units. A question that got no figure goes into a log with no name and no chat ID

Why the language model doesn't do the arithmetic

This part of the bot is valuable because people trust its figures. A model with free access to the data will sooner or later produce a plausible number out of thin air. One such case in a conversation with the owner would undermine trust in every other report. So code makes the decision: keywords in two languages, a fixed set of metrics, a call into the core.

If a question isn't recognised, the metrics module stays silent and the bot answers from the knowledge base. I chose accuracy on purpose: the bot gives a figure only where it can be checked.

So that new metrics follow real demand, questions that got no figure are saved to a log. It keeps only the question text, cleaned of secrets, with no name, username or chat ID. A two-week summary shows what the bot is missing.

The bot reports three kinds of failure in three different ways

If the module is missing, it is switched off and the bot answers from the knowledge base. If the module is there and Shopify is unreachable, the bot says so plainly: "couldn't fetch live data". If the modules no longer agree on the data format, the user sees a message too. A silent failure had already killed this feature once: the module was written for a format that didn't exist, quietly fell back to the knowledge base and looked as if it worked. An error nobody sees can survive for years.

Geography: an order without an address stays in the total

The country comes from the shipping address, where the goods actually went. Orders without an address (digital goods, pickup) count in the total and aren't assigned to any country. Otherwise they would quietly end up in the US and distort the picture. The bot reads years before days, so "for 2025" gives the calendar year, and "besides the US and Canada" removes two countries from the answer. The bot now answers the owner's question word for word: orders, revenue and the number of countries for each year, with sales abroad on a separate line. In 2025 orders went to 21 countries, 19 of them outside the US and Canada.

Top products: money and units side by side

In this business the two metrics differ a lot: accessories bring about 22% of the money and about 63% of the units. A single column would lead to the wrong conclusion about the range. So each item shows both shares, and a separate block lists products that sell in large numbers without making the money top list. Products are grouped by their catalogue ID: the same product can appear under different titles, and the SKU is empty on about 87% of line items. I moved the top-products check ahead of the revenue check. That was the first of two fixes to the order of checks.

US states by sales channel

The owner asked for net sales by state, in total and for the marketplace separately, after refunds and discounts. Net sales here means line items after discounts minus refunded items, without shipping or taxes. The core counts by the same rule, so the state totals have to add up to the overall report.

The core's data had no sales channel before. I added the order source and tags to the request and cleared the old cache. The owner gets a CSV file and a bot answer built from the same numbers.

The question "by US state" contains "US", and with the wrong order of checks the bot sent it to the country breakdown. The second fix to the order: states are checked before countries.

03 · Technology

All the arithmetic lives in one module, and the rest only collect data

Python

Reading the question, the collector modules, the metrics core and sending the answer, all in one language

Shopify Admin API

Orders with line items, refunds, shipping address, source and channel tags. Read-only access

Metrics core

One rule for which orders count, refunds per line item, discounts spread across items, the period and the store's time zone

Keyword matching

Russian and English, with a strict order of checks: states → countries, top → revenue, years → days

Telegram Bot API

Questions and answers in direct messages. The same bot answers from the knowledge base

Question log (JSONL)

Questions that got no figure: text without secrets and without personal details. A 14-day summary

AI agents for review

Four separate reviews of the new code an hour after release; five reviews from different angles before the state report

GraphQL control query

Refunds checked against Shopify's final payment: sales abroad matched to the cent, the year total within 0.1%

pytest

5 guard tests on the calculations, 27 on the state breakdown, 1,312 in the full run

PythonShopify Admin APIGraphQLTelegram Bot APIpytestJSONLCSV
Size of the code

Five Python modules (reading the question, collectors for countries, top products and states, the question log) come to about 1,200 lines, plus about 600 lines of tests. Each collector fits in 150–300 lines because it takes its calculations from the core.

Size of the checks

Four agents an hour after release, and five reviews from different angles before the state report. A check on 30 days of live data: 324 orders in all three reports, zero gap in product revenue. The full test run: 1,312 tests passed.

04 · Results

Four errors an hour after release, and zero discrepancies after the fixes

Metrics with live figures
2 6

added: countries, years, top by money and by units, states by channel

Product revenue overstated in the top list
+8.54% 0.00

checked on 30 days of live data: 324 orders in all three reports

Free orders in the state counts
264 0

orders with a 100% discount over two years: gifts are now separate from sales

An hour after release: four agents, four errors

Four AI agents reviewed the new code, each on its own. Here is what they found:

  • The top-products module didn't subtract refunds at all, so product revenue was overstated by 8.54%.
  • The collector had its own list of payment statuses, and two of the core's statuses were missing from it. That's why the report had 324 orders and the metrics 323.
  • Products were grouped by a "product type" field that line items don't have, because the type lives in the catalogue. The type column had always been empty.
  • The geography module counted a cancelled payment authorisation as a refund. There were no such amounts at the time, so the bug would have waited for the first cancelled authorisation.

All four errors had one cause: the collector did its own arithmetic. Now every calculation goes through the core: one rule for which orders count, refunds per line item, discounts spread across items. The geography module now uses the core's data too, with the shipping address added to it. The cache had to be cleared, or for six hours the bot would have read old data without that field. A check after the fixes on 30 days of live data: 324 orders in the report, in geography and in the top list, and a product-revenue gap of 0.00.

Five guard tests on top of the findings

A figure calculated almost correctly is impossible to catch by eye. So the match with the core is pinned down by tests. Geography and the top list count exactly the same orders as the core. A fully refunded order adds no revenue. A cancelled authorisation doesn't count as a refund. A separate test reads the source code and checks that no collector has its own filter by payment status. If such a filter appears, the automatic test run fails.

Before the state report: four more findings

The owner asked for the state report in the evening and got it the same evening. Before the figures went out, they were checked five times, each time from a different angle:

  • "Draft" and "marketplace order" are two different flags. The phrase "marketplace orders are marked as drafts" invited a filter on the draft flag. I checked seven weeks of data. In one store, 81 of 106 drafts belonged to the marketplace. The other had 12 drafts and no marketplace orders at all. A draft filter would have given the marketplace 25 orders that weren't its own and invented 12 where there was no marketplace. The channel is now taken from the order tag, and drafts are shown on a separate control line.
  • Gifts to bloggers were counted as sales. 264 orders over two years had a 100% discount set by hand. No money came in, but the status said "paid", so these orders were counted in the figures for every state and every country. Paid and free orders are now separate. A gift that cost nothing from the start is told apart from a sale that dropped to zero after a refund.
  • Shopify stores a refund in two forms, and neither covers every case. Only one was counted, and annual revenue came out about 2% too high. Switching to the other fixed the US and Canada but broke sales abroad, where shipping and tax are part of the payment. The solution works order by order: if there is a payment record, use it; if not, use the line items. A control query confirmed it: sales abroad matched to the cent for both years, and the year total within 0.1%.
  • The label "full year" covered 7.6 months. Shopify accepts an end date in the future without an error. Next to the complete previous year, growth of a third looked like a 17% drop. The current year now ends at today's date.

27 new tests pin down exactly these findings; the full run is 1,312 passing tests.

What changed day to day

The owner asks about sales in the messenger and gets live figures from both stores: countries, years, the top list by money and by units, states and channels. The state report reached him as a CSV file and as a bot answer, both built from the same numbers. Questions without a figure collect in the log, and new metrics are added according to real demand.

05 · Where it fits

Where else the same approach works

Behind this case is a common problem: "three reports give three different revenues". It comes up wherever the same figure is calculated in several places: in a bot, on an analytics page, in an export for the accountant.

  • → Online stores on any platform: the owner asks in a messenger and gets a figure calculated by the same code as the official report
  • → Sales across several channels and marketplaces: the channel is taken from a flag that was checked against the data, and drafts and gifts are kept apart from sales
  • → AI assistants with access to an accounting system: the model writes the answer and searches documents, code does the arithmetic, and an unrecognised question goes into the log
  • → Summaries for management: refunds, cancellations and discounts are subtracted in one place, and the period label matches the data
  • → Teams with several reports: guard tests keep every module on one core, and a discrepancy is caught when the tests run

Which channel brings the orders is shown by end-to-end attribution, which follows the path from click to order. This case answers another question: how much was sold, to whom and where, and whether that figure can be trusted.

What I reuse in later projects
  • Keyword matching with a strict order of checks, plus tests that catch a question sent to the wrong metric
  • One metrics core: the rule for which orders count, refunds, discounts and the period all in one place, while the collectors only add things up
  • A guard test that reads the source code and stops a collector from having its own status filter
  • A review of new code by several AI agents, each looking for errors, before the figures reach the client
  • A log of unanswered questions as the plan for the next metrics
Similar challenge?

Three reports give three different revenues and nobody knows which to trust? That can be solved

The place to start is one metrics core and one check against the official report, down to the cent. The bot with live figures comes once the calculations match, and every new metric is built on the same core.

Ready to start?

Start with the 5,000 ₽ audit: a written report and a payback estimate

I'll tell you what to automate first, when it will pay for itself and whether your task needs AI at all. Sometimes it doesn't.

Or just ask a question. During business hours I reply within 2 hours