Skip to content

feat(reports): the four questions a back office asks, behind reports.read - #25

Merged
ArtRiv merged 4 commits into
mainfrom
feature/reports
Aug 29, 2026
Merged

ArtRiv merged 4 commits into
mainfrom
feature/reports

Conversation

@ArtRiv

@ArtRiv ArtRiv commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Item 3 of docs/admin-api.md — reports.read has been in
the permission catalogue since auth with no route behind it. Four read-only
aggregate routes, and nothing else: the AVESSO panel asked four questions, and
these answer exactly those.

Spec first, as usual: docs/specs/reports.md.

GET /reports/product-sales units and revenue per piece, over a period
GET /reports/revenue revenue by week or month, as a continuous series
GET /reports/carts what is sitting in carts right now
GET /reports/unsold-products ACTIVE, in stock, no sale in the period

What counts as a sale — half the spec, and the point of it

PAID, SHIPPED or DELIVERED, timed by paidAt. One SOLD_STATUSES
constant, used by all four routes including "not moving", so the two screens
cannot contradict each other.

CREATED is out, as the survey's own warning predicted: the stock left the
shelf but nothing was paid. CANCELLED and REFUNDED are out too — and the
argument got stronger on inspection, because both call
StockService.restock
, so excluding them is also what keeps "units sold" and
"stock that left the shelf" agreeing.

The clock is paidAt, never createdAt: an order created on the 31st and paid
on the 1st is the new month's revenue, which is how the bank statement sees it.

Two things the four questions did not anticipate

paid_at is TIMESTAMP(3) without a zone. So paid_at AT TIME ZONE $tz
converts in the opposite direction — it declares the naive reading was
already local. The SQL anchors in UTC first:

date_trunc($granularity, (o.paid_at AT TIME ZONE 'UTC') AT TIME ZONE $tz)

A silent three-hour error that any test written around midday would miss, so
there is an e2e that puts a 01:30 UTC September sale into the store's August.
The zone is REPORTS_TIMEZONE (default UTC, validated at boot) — cutting a
Brazilian store's weeks in UTC pushes every Sunday evening into the following
Monday.

The 409 from variant removal counts cart lines, not units. /reports/carts
answers SUM(quantity), with lineCount and cartCount alongside — 40 units
across 2 carts and 40 across 30 are opposite situations. cartCount comes from
the lines, because checkout leaves the cart row alive and empty.

Two decisions worth arguing with

These answer 403, not 404. The house pattern protects the existence of a
resource
from being confirmed to whoever probes an id. No route here takes an
id of any kind, so there is nothing whose existence could leak — a 403 only
reveals that /reports exists, which the published OpenAPI already says. This
PR also qualifies the survey's blanket "every new back-office read must answer
404" to say where it applies. Spec invariant 10.

reports reads tables it does not own. The one place in the repo that
crosses a module boundary at the database rather than through a service. The
alternatives were report-shaped methods (date_trunc, week GROUP BY) hanging
off OrdersService and ProductsService, or summing money in JavaScript — which
the spec forbids outright. It is safe because of what the module cannot do: only
SELECTs of aggregates, no writes, and it exports nothing, so nothing can depend
on it. Spec invariant 11, and docs/architecture/modules.md
now records it as the exception rather than leaving it to be discovered.

Verification

lint:check, typecheck and build clean. 545 unit tests (43 new) and
258 e2e (46 new), the full e2e suite green against the disposable e2e
schema. openapi.json regenerated — 46 operations, four more, in a ninth
controller.

The split between the suites is deliberate. The unit tests read the bound
parameters
back to prove CREATED, CANCELLED and REFUNDED never reach the
database as something to count — an assertion about how the query is built, which
no mock can hide. What only a real Postgres falsifies is the SQL: the timezone
bucketing above, that a month with no sales returns zeros rather than a gap, and
that "not moving" is the exact complement of "sold" against the same rows.

The e2e writes orders straight to the database rather than driving checkout,
because paidAt is stamped by the state machine and no route backdates it — a
suite driving checkout could only ever report on today.

Left out on purpose

Size breakdown, cart value, refunds as their own report, period-over-period
comparison, export, average ticket, conversion. Each is registered in the spec's
deferred decisions with its reason. The panel asked four questions.

🤖 Generated with Claude Code

ArtRiv and others added 4 commits August 29, 2026 15:13
Item 3 of docs/admin-api.md: the permission has existed since auth with
no route behind it. Four read-only aggregates, and half the spec is about
what counts as a sale — CREATED is not money yet, and CANCELLED/REFUNDED
both restock, so neither is revenue either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…read

Four read-only aggregate routes, gated on the permission that has existed
since auth with nothing behind it. Aggregation happens in Postgres; the
Node side only transports.

One definition of a sale, shared by all four: PAID, SHIPPED or DELIVERED,
timed by paidAt. CREATED is stock off the shelf with no money behind it,
and CANCELLED/REFUNDED both restock — so leaving them out also keeps
'units sold' and 'stock that left the shelf' agreeing.

Weeks and months are cut in REPORTS_TIMEZONE (default UTC), and the SQL
anchors the naive TIMESTAMP column in UTC before moving it there: the
short 'AT TIME ZONE tz' form converts in the opposite direction, which is
a silent three-hour error for a Brazilian store.

They answer 403, not 404 — the 404 pattern protects the existence of a
resource, and no route here takes an id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The unit tests prove what gets bound; only a real database falsifies the
query. Three claims in particular: a sale at 01:30 UTC on 1 September
buckets into AUGUST for a Sao Paulo store, a month with no sales comes
back as zeros rather than as a gap, and 'not moving' is the exact
complement of 'sold' against the same rows.

Orders are written straight to the database: paidAt is stamped by the
state machine and no route backdates it, so a suite driving checkout
could only ever report on today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes item 3 of the admin-api survey, out of the order suggested there:
the AVESSO panel already existed and already asked. The survey's own
warning that 'derived revenue is easy to get wrong' became half the spec,
and its three traps were real.

Also qualifies the survey's 'every new back-office read must answer 404':
that pattern protects the existence of a RESOURCE, so it applies to routes
that take an id. These four take none.

The test counts in status.md were stale since variant management; they are
now the verified figures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ArtRiv
ArtRiv merged commit 88f72d5 into main Aug 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant