Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions API/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ persistence:
| Orders | `/v1/orders` | Draft orders, checkout, cancellation, return requests |
| Inventory | `/v1/admin/inventory` | Stock levels and reservations |
| Admin | `/v1/admin/orders` | Back-office fulfillment |
| Analytics | `/v1/admin/analytics` | Commercial reporting over the order history |
| Returns | `/v1/admin/returns` | Admin decisions on return requests |
| Webhooks | `/v1/webhooks` | Inbound payment provider callbacks |
| Health | `/health` | Liveness and readiness |
Expand Down Expand Up @@ -112,6 +113,71 @@ See [Orders and Fulfillment](/Orders-and-Fulfillment).
| `POST` | `/v1/admin/orders/{order_id}/ship` | Mark order as shipped | admin |
| `PATCH` | `/v1/admin/returns/{return_id}` | Approve, reject or complete a return | admin |

### Analytics — `/v1/admin/analytics`

Commercial reporting, computed in SQL over **all** orders rather than a recent sample.

| Method | Path | Purpose | Auth |
|---|---|---|---|
| `GET` | `/v1/admin/analytics/summary` | Revenue, refunds, AOV, units, vs the previous period | admin |
| `GET` | `/v1/admin/analytics/timeseries` | The same figures bucketed by day, week or month | admin |
| `GET` | `/v1/admin/analytics/products` | Per-SKU units, revenue, return rate, unsold stock | admin |
| `GET` | `/v1/admin/analytics/funnel` | Where orders stop | admin |

All four take optional `from` and `to` calendar dates, inclusive, defaulting to the last 30
days. A window longer than five years is refused: the figures are computed live rather than
from a rollup table, so an unbounded range is a slow query waiting to happen.

#### What counts as revenue

These are choices, not facts, so they are written down rather than left implicit in a query:

| Term | Definition |
|---|---|
| Gross revenue | Orders in `paid`, `ready_to_ship` or `shipped` |
| Refunded | Orders in `refunded` — **whole-order only** |
| Net revenue | Gross less refunded |
| Average order value | Gross divided by revenue-producing orders |
| Units | Line quantities on revenue-producing orders |

Never counted: `draft`, `pending_payment`, `cancelled`, and anything soft-deleted.

#### Money is grouped by currency

Every money-bearing response is a **list keyed by currency**, not a single figure.
`orders.currency` permits more than one, and a total summed across currencies is not
slightly wrong — it is meaningless. For the usual single-currency shop the list has one
entry and reads like a plain number.

A client must not add these together. The response shape exists to make that mistake
visible rather than silent.

#### Days are cut in the shop's timezone

Buckets use `SHOP_TIMEZONE` (default `Europe/Berlin`), not UTC. An order placed at 23:30
UTC belongs to the next day in Berlin, and bucketing on UTC would file a day's takings
against the wrong day — an error that looks like a data problem for weeks. See
[Configuration](/Configuration).

#### What these numbers cannot tell you

Stated plainly, because a reader will otherwise infer something stronger:

- **The funnel is an order funnel, not a visitor funnel.** It begins at order creation and
cannot see shoppers who browsed without ordering. Visitor conversion needs session data
the API does not collect.
- **Partial refunds are not modelled.** `orders.status = refunded` is all-or-nothing, so
refund figures will not reconcile against a partially refunded Stripe charge.
- **Per-SKU return rate is an upper bound.** Returns are recorded per order, not per line,
so a return on a two-line order counts against both SKUs.
- **Product revenue need not equal order revenue.** Product figures sum line values; an
order total may carry shipping or adjustments belonging to no line.

Checkout is counted from the `payments` table rather than from `orders.status`. Status
records only where an order is *now*, so a cancelled order is indistinguishable from one
that never reached checkout, and an order that shipped then was refunded no longer says it
shipped. A payment row is written when checkout starts and survives whatever follows.

### Inventory — `/v1/admin/inventory`

Stock is deliberately separate from the catalogue. The `inventory` block on an item is
Expand Down
11 changes: 11 additions & 0 deletions Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@ outside Compose or against a Dashboard-managed endpoint.
Too short and a slow payment loses its reservation mid-flow; too long and abandoned carts
hold stock nobody can buy.

## Analytics

| Setting | Default | Description |
|---|---|---|
| `SHOP_TIMEZONE` | `Europe/Berlin` | IANA timezone the shop trades in |

Analytics buckets days in this zone rather than UTC, so "today" matches the operator's day.
Leave it wrong and evening orders land on the following day for any shop east of Greenwich —
the figures stay internally consistent, which is what makes it hard to notice. An unknown
zone is rejected at request time with a `422` naming the value, rather than a `500`.

## Object storage — MinIO / S3

| Setting | Default | Description |
Expand Down
1 change: 1 addition & 0 deletions home.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ system. It is one possible shop UI, not *the* shop UI — that is the point of t
| Fulfillment | Carrier labels, packing slips, the job queue and outbox |
| Shipments | Tracking numbers and label files |
| Returns | Customer return requests and admin decisions |
| Analytics | Revenue, product performance and the order funnel |
| Health | Liveness and readiness probes |

The full endpoint list is on the [API Architecture](/API/Architecture) page, and a live,
Expand Down
20 changes: 20 additions & 0 deletions openapi.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@
"summary": "Readiness check"
}
},
"/v1/admin/analytics/funnel": {
"get": {
"summary": "Order funnel (admin)"
}
},
"/v1/admin/analytics/products": {
"get": {
"summary": "Product performance (admin)"
}
},
"/v1/admin/analytics/summary": {
"get": {
"summary": "Commercial summary (admin)"
}
},
"/v1/admin/analytics/timeseries": {
"get": {
"summary": "Revenue and volume over time (admin)"
}
},
"/v1/admin/inventory/": {
"get": {
"summary": "List inventory items (admin)"
Expand Down
Loading