Skip to content

Powered by Grav CMS + Helios Theme

Reports and the rollup

KahunaCart answers a sales report from a daily rollup rather than by reading every order. This page explains what the rollup holds, when its numbers are exact, and how to rebuild it. It is for a store administrator running reports, and for anyone importing orders around the admin.

The reports themselves (what each one shows, the date range, the CSV and PDF exports) are on Orders and the admin. Analytics covers reading them: comparison periods, filters, breakdowns, where an order came from, cost of goods and margin, and saved views.

What the rollup is

KahunaCart stores one row per finished day instead of recomputing the day from the orders table every time somebody opens a report.

Table One row per What it holds
kahunacart_order_stats day × currency × payment provider orders, items, revenue_minor, refunded_minor, tax_minor, shipping_minor, discount_minor
kahunacart_product_stats day × currency × product × variant title, qty, revenue_minor, refunded_minor

day is the start of a UTC day as a Unix epoch, matching every other timestamp in the schema. Money is minor units. Currency is a dimension and never a conversion: a store trading in two currencies gets two rows a day and nothing adds them together.

The first time a recompute writes a row in a currency other than the base, it also writes the settings row reports.other_currencies. That row is how a store that has switched its second currency off again still reports one currency at a time while its euro orders are still in the history. It is written once and never cleared.

tax_minor, shipping_minor and discount_minor are signed exactly as the adjustments hold them, so a discount is negative. No report reads them yet.

What a report actually reads

Every native per-currency report splits its range at a watermark: reports.stats_through in kahunacart_settings, the last day the rollup is authoritative for.

  • Days up to and including the watermark are read from the rollup.
  • Days after it, today included, are scanned out of the orders table.

Today is never rolled up. A day is only finished once every refund, webhook and correction that names it has landed, so the watermark never reaches the current day.

Note

A store with no rollup has no watermark and scans the whole range. The numbers are identical and only the speed differs. No state of these tables makes a report wrong.

Run bin/plugin kahunacart status to see the watermark. "not built" means every report is scanning the orders table.

Converted reports

The All, in the base currency choice (currency=all) doesn't use the rollup. It reads the orders with the rate each one was placed at, their current lines, adjustments and refund transactions, adds the exact decimal amounts and rounds each figure once in the base currency. A rollup row holds one native total for a day, and it can't reproduce different order rates within that day. No rollup column or migration was added for this, and converted reports need no rebuild.

Their cost grows with the orders and their lines in the range. Product refunds also read each credited order's current lines, and sorting customers by value reads every matching customer. Native reports and their rebuild costs stay as they were. Rebuilding all native history still recomputes each closed day and its per-currency rows, but it can't fill in exchange rates that were never recorded. See Reports and totals.

When the numbers are exact

These are exact at all times.

  • Orders, units and revenue for any day, including a day changed after it closed.
  • Refund totals, bucketed on the refund's own date.
  • Top products by revenue and by units.

They stay exact because completing, cancelling, refunding or repricing an order on a closed day recomputes that day on the spot. A refund against last month's order drops out of last month's revenue immediately.

These are not exact until the next rebuild.

  • Editing the lines of an order on a closed day: adding, removing or requantifying an item.
  • Recalculating an order on a closed day, which rewrites its tax, shipping and discount adjustments.
  • Any write that goes straight into the tables rather than through the admin or the API: an importer, a SQL script, or a restore that carried the orders and not the rollup.

Run bin/plugin kahunacart reports:rebuild after any of those.

Note

kahunacart_product_stats.refunded_minor counts refunds recorded against a line. An amount-only refund has no line to attribute to, so it lands in the day's refunded_minor on the order rollup and nowhere else. The admin's Products report tab still spreads those proportionally at read time.

How the rollup is maintained

Who When What it does
Migration 0032 Once, on upgrade Backfills every closed day and sets the watermark
bin/plugin kahunacart work Every scheduler tick Recomputes the last 3 closed days, and catches up 60 days a tick when the rollup is behind
OrderRepository On every order write Recomputes a closed day whose numbers just changed
bin/plugin kahunacart reports:rebuild On demand Recomputes a range, or all of history

Recomputing a day is idempotent. The row is rebuilt from that day's rows rather than adjusted by a delta, so running a rebuild twice gives what running it once gave, and that matches a full scan of the day.

Rebuild the rollup

  1. Run bin/plugin kahunacart reports:rebuild for all of history, or add --from=YYYY-MM-DD --to=YYYY-MM-DD for one window.
  2. Read the last line. It says how many days were recomputed and the date the rollup is now authoritative through.

The reports read the rebuilt days from the next request. See CLI for the flags.

Ranges longer than a year

There is no cap on a report's range. A store trading since 2014 reports every year of it.

A range over 400 days comes back one row per calendar month rather than one per day. The bucket field in the payload reads month instead of day, and the date on each row reads 2026-08 instead of 2026-08-14. A long series starts at the first month the store traded rather than at the range's own start, so asking for everything does not draw five hundred empty months.

Foreign-currency history is recorded as soon as a cart or order is written. Disabling the currency before the first daily rollup does not mix those amounts into base-currency reports. The currency notes and filters that remain are intended.