# Metered billing and cost allocation

> How billing periods, rate plans, metered units, projections, tag-based cost allocation, voice call rating, prepaid top-ups, plan pools and invoices fit together.

Billing in the TeleQuick console is assembled from two independent
systems that are presented side by side:

| System | Covers | Source of truth | Currency label |
| ------ | ------ | --------------- | -------------- |
| **Fleet metering** | Robotics and Games usage | aggregated usage rows, persisted per period in `fleet_usage_period` | the `currency` field on the usage response (the screen falls back to `USD`) |
| **Voice rating** | Per-call charges and the prepaid wallet | the CGRateS rating engine, plus rated CDRs in ClickHouse | unit-less in the engine; labelled from the deployment's configured currency |

They are not the same ledger, they are not aggregated by the same job,
and they do not have to agree with each other. Most confusion on the
billing screens comes from reading a number from one system against a
number from the other.

## What a billing period is and when it closes

The fleet billing screen works on one period at a time. `fleetBilling.getUsage`
returns `periodStart` and `periodEnd`, which the header renders as
`Period: <start> → <end>` and the KPI strip renders as a month label.

A period becomes *history* when a row for it exists in `fleet_usage_period`.
`fleetBilling.history` reads those rows — the screen asks for the last
6 periods — and renders one table line per period with the metric names
it contains and the period total. The empty state is explicit about the
ordering:

> No history yet. This month's row lands once usage is aggregated.

So: the current period is a live calculation, and the history table only
shows what has been aggregated into a period row. A month you are still
inside may be absent from *Recent months* while being fully populated in
*This period · metered usage*.

For voice, the period is the **current calendar month**. `cdr.monthStats`
filters CDRs with `toStartOfMonth(starting_time) = toStartOfMonth(now())`,
and `billing.invoices.generate` snapshots the current calendar month's
metered CDR cost. There is no configurable billing anniversary on either
screen.

## Rate plans and what a metered row contains

Each vertical has its own rate plan. The plan is a list of **metrics**, and
one metered row is rendered per metric in the plan — including metrics that
metered nothing. Each row carries:

| Field | Rendered as |
| ----- | ----------- |
| `metric` | the row key; also the name listed in the *Recent months* table |
| `rate_plan_name` | the row title |
| `rate_plan_description` | the explanatory line under the title |
| `rate_per_unit` + `unit_label` | the `$X/<unit>` price chip |
| `units` | the counted quantity for the period |
| `cost_usd` | the money for that row |

If the plan itself has no entries, the panel reads
`No billable metrics yet — rate plan is empty for <vertical>`. That is a
plan-configuration state, not a usage state: it means nothing is priced for
that vertical in this deployment, so nothing can be charged.

The **Metrics billed** KPI counts only rows with `units > 0`, shown as
`n of <total rows>`. A low `n` against a large total is normal — it means
most of the plan's metrics were not exercised this period.

Units are formatted at different precision depending on magnitude (whole
numbers with thousands separators for large values, four decimal places
below 1), so a row can legitimately show a very small non-zero quantity.
The quantity you see is the quantity that was multiplied by `rate_per_unit`.

## Month-to-date total vs the end-of-period projection

Two different figures sit next to each other in the KPI strip:

- **MTD cost** — `total_cost_usd`. Money already metered in this period.
  It is also repeated as **Period total** at the foot of the metered rows,
  and it is the figure the tag split reconciles against.
- **Projected end-of-period** — `projected_end_of_period_usd`, computed
  server-side and returned alongside the total.

The projection is an estimate produced by the usage aggregation, not a
commitment and not an invoice. Only the MTD total corresponds to usage that
has actually been metered. Do not chargeback, alert on, or reconcile against
the projection.

Both figures are formatted for readability, not for accounting: values at or
above 1000 are rounded to whole currency units and thousand-separated, values
at or above 100 are shown without decimals, and only smaller values show
cents. A displayed `$1,204` is a rounded render of the underlying number.

## Aggregation timing and refresh behaviour

Fleet aggregation is not a background job you wait on. As the footnote on the
screen states:

> Aggregation runs inline on each query — re-render to refresh.

Concretely:

- `fleetBilling.getUsage` re-runs on an interval of 120 s while the screen is
  open, and the header shows either `Refreshing…` or `Updated <relative time>`
  from the last successful fetch.
- The **Refresh** button re-runs the same query, and is disabled while a fetch
  is in flight.
- `fleetBilling.history` is cached for 5 minutes, so a period row that has just
  landed may take until the next fetch to appear in *Recent months*.
- `tags.keys` is likewise cached for 5 minutes.

The voice side has no polling. The portal refetches the account balance and the
payment list when the Add-credits modal settles, and nothing else refreshes on
its own.

## Cost allocation by tag: allocated, unallocated, unattributable

The *Cost by tag* panel splits this period's spend by the values of **one tag
key**. Tag keys come from `tags.keys`, which reads the `resource_tag` table for
the org, counts rows per key, and returns keys ordered by usage count (ties
broken alphabetically). The panel defaults to the most-used key; the dropdown
selects another.

Tag values are inherited. Tagging a site or fleet (robotics) or a region or
server fleet (games) is enough — the resources under it inherit the value, which
is why the split works without tagging every individual robot or server.

`fleetBilling.costByTag` returns three distinct kinds of money, and the panel
deliberately shows all three:

| Bucket | Field | Meaning |
| ------ | ----- | ------- |
| **Allocated** | `groups[]`, `allocated_cost_usd` | Spend that carries a value for the selected key. Each group reports its `value`, the number of `resources` behind it, its `cost_usd`, and a per-metric unit breakdown. A group whose value is the empty string renders as `(no value)`. |
| **Unallocated** | `unallocated.cost_usd`, `unallocated.resources` | Resources that ran and cost money but carry **no value** for this key. Rendered in the warning colour, because this is real spend with nobody's name on it. |
| **Unattributable** | `unattributable[]` | Metrics that have **no per-resource dimension at all**, each with a `metric`, a `cost_usd` and a `reason`. No tag key can ever split these. |

The distinction matters before a chargeback goes out: *unallocated* is a tagging
gap you can fix, *unattributable* is a property of the metric and will not
improve by tagging more resources. Groups and unattributable entries are only
rendered when their cost is non-zero; if nothing metered this period carries the
key at all, the panel says so by name.

When the organisation has no tags yet, the panel renders guidance instead of an
empty chart — there is nothing to group by.

Voice has an equivalent, `billing.costByTag`. It splits the period's rated CDR
cost by the values of one tag key along a chosen `dimension` — `trunk`
(the default) or `phone_number` — with an optional `periodStart`. It exists
because `billing.account` returns a single balance for the whole tenant, which
cannot answer "which cost centre burned it", whereas each CDR carries a rated
cost and a trunk. It deliberately does **not** stamp a currency on its result;
see [Currency handling](#currency-handling). If the allocation query fails, the
procedure raises `Voice cost allocation failed.` rather than returning a
plausible-looking partial split.

## Reconciling a tag split against the period total

The panel renders a reconciliation line instead of asking you to trust the bars:

```
allocated_cost_usd + unallocated.cost_usd  ==  total_cost_usd
```

The two sides are compared with a tolerance of one cent. When they match, the
line reads *Allocated + unallocated matches the period total*. When they do not,
it reads *Split does not match the period total — some usage is not
resource-attributed*, and the accounted-vs-total figures are printed beside it
as `<accounted> / <total>`.

A mismatch is expected whenever unattributable metrics carry cost: that money is
in the period total but, by definition, in neither the groups nor the
unallocated bucket. Read the unattributable lines to see how much, and for which
metrics.

## Per-tenant rate overrides in fleet billing

The fleet billing screen states its own limitation:

> Rates are platform defaults. Per-tenant overrides land in V2.

Every `rate_per_unit` on that screen is the platform default for the vertical's
rate plan. There is no per-tenant override applied to fleet metering from this
screen. (Voice-side prices are a separate mechanism — see
[Rate cards and who may change a price](#rate-cards-and-who-may-change-a-price).)

## The rating account and when it is created

Voice call rating happens in CGRateS. `billing.account` calls
`ApierV1.GetAccount` with the org id as the CGRateS **tenant** and, by default,
the account name `default`. It returns `found`, the account `id`, a `disabled`
flag, and the monetary `balance`.

The account is **created lazily, on the first rated call**. There is no
provisioning step. This is why the console distinguishes four states, computed
once and shared by the *Account Balance* tile and the *Rating Engine* card so
they cannot contradict each other:

| Status | Condition | What the card says |
| ------ | --------- | ------------------ |
| `error` | `billing.account` or `cdr.monthStats` returned an error | **Unreachable** — balance and history are unavailable |
| `connected` | an account id came back | **Connected**, with the account id and balance |
| `pending` | Voice is subscribed (per `hub.overview`) but no account exists yet | **Active — awaiting first call** |
| `inactive` | Voice was never started for the workspace | **Not activated** |

`pending` is the state most often misread as a failure. "Has a rating account
been auto-provisioned yet" and "did the org start the Voice product" are two
different facts: the first comes from CGRateS, the second from
`organization.subscriptions.voice`, which `hub.setSubscription` flips
immediately. Voice being **Active** on the Hub while the rating engine reports
no account is the normal state of an activated workspace that has not yet placed
a call. The balance tile says `active — bills on first call` in that state.

## The flat voice tariff: per call, per minute, per-second rounding

The portal's *Pricing* card describes one tariff that applies to every call,
inbound or outbound:

| Component | Displayed price | Applied |
| --------- | --------------- | ------- |
| Per call | `0.10` | once, at call setup |
| Per minute | `0.50` | per second, rounded up to the next second |

The *Rating Engine* footer restates the same thing as **Billing Mode:
Per-second, flat tariff**, with **Rating Precision: 6 decimals**.

Two things to know about these two numbers:

- They are **display constants in the portal bundle** that mirror what the
  CGRateS seed script writes (`RT_CONNECT`'s ConnectFee and `RT_PER_MIN`'s
  Rate). Changing the price means editing both the portal constant and the seed
  script, then re-running the seed. Editing only one makes the screen lie.
- Trunk numbers are **not** part of this tariff. They are purchased separately
  from the Trunks page (one-time and/or monthly per number). Once a number is
  owned, calls on it rate at this flat plan.

## Wallet balance vs metered spend

These are different measurements and are expected to disagree:

- **Account Balance** is the CGRateS `*monetary` balance — a prepaid wallet
  figure, moved by top-ups and by rating.
- **Month-to-Date Cost** / **Spend this month** is `cdr.monthStats.cost`, the sum
  of rated cost over this calendar month's CDRs, read from ClickHouse.

`cdr.monthStats` deduplicates before summing: it groups the `cdrs` table by
`call_id`, takes `max(duration)` and `max(cost)` per call, and then sums those.
Multiple CDR rows for the same call therefore contribute once. The same query
returns `calls` (distinct calls) and `duration_sec`, from which the screens
derive **Total Minutes** / **Minutes this month**.

Because the wallet spans all time and includes top-ups, while metered spend
spans one month and includes only rated calls, the two numbers answer different
questions. Use the balance to answer "can this tenant still place calls", and
metered spend to answer "what did this month cost".

The portal also renders a cross-vertical strip from `hub.overview`, summing
`cost_mtd_usd` across voice, robotics and games. That strip refuses to print
`$0.00` when the query failed or is still loading — it shows
