> ## Documentation Index
> Fetch the complete documentation index at: https://omniflow-vpn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Sales and payment reporting

> What was sold over a period you choose, whether the money arrived, and the rules that decide which figures are ever added together

`/admin/reports` answers the questions the dashboard cannot: how a quarter went,
which plan is actually selling, how much of the money is renewals rather than new
business, and whether the trials turn into anything.

The dashboard's thirty-day window stays exactly where it is. A window that moves
cannot be compared between two visits, which is the property an operations
overview needs and the reason it was fixed in the first place. This is the screen
where the window moves.

## Three rules that decide the numbers

**The period keys on when the order was paid, and that instant never moves.**
`orders.paid_at` is written the first time an order reaches a settled state and
never again. Before this existed, reporting keyed on `updated_at` — so recording
a refund in March silently moved a February sale into March, and the same report
run twice over the same closed period returned different numbers. An integration
test now records a refund against a closed period and asserts the figures do not
change.

**Provider money and wallet credit are never added.** They are separate columns
in every table on the screen. A wallet balance was already counted as revenue
when it was funded, so adding the two counts it twice — which is exactly the
mistake a single "revenue" figure invites.

**Refunds are reported on the day they were issued**, not against the sale they
reverse. "What did we refund in March" is a question an operator can act on;
"what did we sell in February that was later refunded" retroactively changes a
month that had closed.

## What the screen shows

| Section | What it answers |
| - | - |
| Trial conversion | Of the trials claimed in this period, how many of those customers have since paid for anything. |
| By kind of sale | New purchases, automatic renewals, extensions, upgrades, downgrades, add-ons, wallet top-ups, gifts, and digital goods, per currency. |
| By plan | Which plan version sold, at which billing period, and what it was worth at list price. |
| Provider money by day | One chart per currency, with the same figures as a table beside it. |
| Refunds issued | What went back, per currency, in this period. |

> [!NOTE]
>
> Trial conversion is a **cohort** measure and the screen says so beside the
> figure. The denominator is trials claimed inside the period; the numerator
> counts conversions at any later time. A period ending today therefore reads
> low — a trial claimed yesterday has had one day to convert — and that is a
> property of the question rather than a fault in the measurement.

## The period

Four presets cover what people actually ask for — 7, 30, 90, and 365 days — and
the two date fields take anything else. A period longer than two years is
refused with the limit named, rather than becoming a query that runs until a
proxy times out.

**Days are bucketed in your own timezone**, taken from the browser and passed to
the API. "Sales on the third" means the operator's third; a report bucketed in
UTC puts an evening sale several hours east on the wrong day, and an integration
test asserts the same sale lands on the 2nd in UTC and the 3rd in Moscow.

The plan breakdown reads the code, version, and billing period off the plan
version each order actually referenced rather than off the plan's current state.
That is what plan versions are for: a price change last week must not re-price a
sale from last month.

## Payment health

The second tab answers a different question with the same period: not what was
sold, but whether the money arrived. With four bundled adapters, an acquirer that
starts refusing cards is otherwise visible as support tickets and a growing
stuck-payment queue rather than as a number that moved.

**Two rates, because two things fail.**

| Rate | Definition | What it measures |
| - | - | - |
| Settlement | settled ÷ (settled + failed) | The provider's own success rate on payments it was asked to take. |
| Completion | settled ÷ (settled + failed + abandoned) | The funnel, including customers who changed their mind. |

A customer abandoning a checkout and a gateway refusing a card are not the same
event, and only the second is the acquirer's. Collapsing them into one "success
rate" produces a figure that drops every time a campaign reaches people who were
never going to buy — which is how a real settlement problem gets lost in noise.

**Intents still in flight are in neither denominator** and are counted apart. One
created five minutes ago has not failed, and including it would make the current
hour look catastrophic exactly when somebody is watching. The oldest open intent
is reported as an age, which is the stuck-payment queue expressed as a number.

**A rate is absent rather than zero when nothing reached a decision.** "Nobody
paid" and "everybody failed" are opposite facts, and a screen showing 0% for a
quiet adapter would send somebody to investigate one nobody used. The sample size
travels beside every rate for the same reason: 50% of four is a coincidence and
50% of four hundred is an incident.

**Time to settle is measured from the settlement event**, not from the intent's
`updated_at`. A later reconciliation touches the row, and keying on it would
inflate the latency of every payment anybody looked at twice.

### Webhook intake

Settlement and notification fail independently: a gateway that takes the money
and cannot deliver the callback looks healthy from the customer's side and
produces a stuck queue here. The table reports what arrived, what was processed,
what failed, and what was **rejected for a bad signature** — a non-zero rejection
count is either a rotated secret or somebody probing the endpoint, and both are
worth seeing.

## The export

`GET /v1/panel/reports/sales/export` takes the same four parameters and returns
one CSV with a `section` column — `operation`, `plan`, `day`, `refund`, `trial` —
rather than four files, because the breakdowns are four views of one period.

Amounts are exported in **minor units, unscaled**, exactly as they are stored.
Dividing by a hundred is wrong for every zero-decimal currency, and it is
something the reader can do knowing their own.

## Permissions

Both routes need `finance.read`, the same permission the order and payment
screens need. A sales report is money, and an installation that has decided who
may read money has already made this decision; a permission of its own would let
the two drift into disagreeing about the same figures.

## What is not here

Advertising attribution and per-node traffic are separate absences tracked in
the roadmap. These screens report what was sold and whether the money arrived,
not where the customer came from or which node carried them.

## Related

> **[Admin operations](https://omniflow-vpn.mintlify.site/operations/admin-operations.md)**
>
> The dashboard, its fixed window, and the incidents and drift beside it.

> **[Orders and payments](https://omniflow-vpn.mintlify.site/commerce/orders-payments.md)**
>
> The states an order moves through and what settles it.

> **[Financial invariants](https://omniflow-vpn.mintlify.site/commerce/financial-invariants.md)**
>
> Why provider money, wallet credit, and refunds are three figures rather than one.

> **[Wallet and ledger](https://omniflow-vpn.mintlify.site/commerce/wallet-ledger.md)**
>
> Where a balance comes from, and why spending it is not new revenue.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.