screentinker/docs/billing.md
ScreenTinker 977407ce99 feat(#146): usage metering + admin-gated Billable Screens report (contract system-of-record)
Implements the ByteTinker-Bold distribution-agreement billing math and surfaces it on a
standalone admin-only route. No UI (the API figure is the deliverable). Server-side only.

Contract math (lib/billing.js, config-driven; defaults ARE the agreement):
- ASD (per device/day) = min(1.0, online_seconds / (hours*3600))   # 28800 default
- BillableScreens (per month) = round-half-up( Sum ASD / days_in_month )
- Flat tier (not marginal): 1-499 $1.50 / 500-999 $1.25 / 1000+ $1.00; cost = screens*rate.
Single global rate card for now (per-tenant is a future concern; noted in code).

Data foundation:
- New durable rollup device_usage_daily(device_id, day 'YYYY-MM-DD', online_seconds),
  index on day. status_log (3d) / telemetry (24h) can't back a billing month.
- Accumulated INCREMENTALLY off the heartbeat tick from the live connection map (same
  source as devices_connected) - never reconstructed from logs. Each tick credits every
  connected device's today-row (min(86400, +elapsed)), chunked + transactional (non-blocking);
  per-tick credit capped (accrualCapSeconds) as a stall/restart guard.
- Retention ~400d, pruned via chunked-prune (pruneUsageDaily in runMaintenance).

API: GET /api/billing/usage?month=YYYY-MM (default current), requirePlatformAdmin, mounted
SEPARATELY from /api/status (billing is revenue data + a heavier aggregate; must not touch
the hot status path). Reads the rollup only. MTD figure averages over COMPLETED days only
(today shown in `daily` but excluded until it completes); is_final + billable_screens_final
appear once the month completes.

Tests (12): ASD math; billable round-half-up; flat tier/cost boundaries; accumulator
(accrues by interval, caps at 86400/day, disconnected doesn't accrue); report MTD-excludes-
today + final-month is_final; retention prune; endpoint authz (admin 200 / non-admin 403 /
anon 401) + billing absent from /api/status. Suite 301/301. First-full-month caveat +
formula in docs/billing.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:45:27 -05:00

67 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Billing — Usage Metering (ByteTinkerBold Media distribution agreement)
**This is the contractual SYSTEM-OF-RECORD for invoicing (agreement §4.1/§4.2).** The math
below is implemented exactly in `server/lib/billing.js`; the defaults in `config.billing`
ARE the agreement — change them only if the contract changes.
## The formula
- **Provisioned Screen** — any device registered/provisioned. Provisioning alone is **not**
billed.
- **Active Screen-Day (ASD)** — per device, per calendar day:
```
ASD = min(1.0, online_seconds_that_day / (BILLING_HOURS_PER_DAY * 3600)) # denom = 28800 (8h)
```
Online ≥ 8h → 1.0; 4h → 0.5; offline → 0. Time beyond 8h does **not** increase it.
- **Billable Screens** — per billing month:
```
BillableScreens = round( Σ ASD (all devices, all calendar days in month) / days_in_month )
```
i.e. the average number of screens active during a standard 8-hour day, **rounded half up**.
- **Tier** — FLAT (not marginal): the single rate for the month's total Billable Screens.
| Billable Screens | Rate / screen / month |
|---|---|
| 1 499 | $1.50 |
| 500 999 | $1.25 |
| 1000 or more | $1.00 |
- **Cost (USD)** = BillableScreens × applicable tier rate.
Single **global** rate card for now; per-tenant rate cards are a future concern (would key
the rate table by workspace/org). All values are config-driven: `BILLING_HOURS_PER_DAY`,
`BILLING_RATE_TABLE` (JSON override), `BILLING_USAGE_RETENTION_DAYS`.
## Data foundation
- **`device_usage_daily(device_id, day 'YYYY-MM-DD', online_seconds)`** — a durable daily
rollup, one tiny row per device per calendar day (`day` is **UTC**). `device_status_log`
(3-day) and `device_telemetry` (~24h) cannot back a billing month, so this is separate.
- **Accumulated incrementally** off the heartbeat tick from the **live connection map**
(`services/heartbeat.js` `deviceConnections` — the same source `devices_connected` uses),
never reconstructed from logs. Each tick credits every connected device's today-row with
the elapsed seconds (`online_seconds = min(86400, online_seconds + credit)`), chunked and
transactional so it never blocks the event loop. Per-tick credit is capped
(`BILLING_ACCRUAL_CAP_SECONDS`) so a stalled loop / restart gap can't inject a bogus credit.
- **Retention** ~400 days, pruned via the chunked-prune helper (`pruneUsageDaily`) so it can
never bloat-then-freeze.
## API (admin-only, standalone route)
`GET /api/billing/usage?month=YYYY-MM` (default: current month) — platform-admin gated,
mounted separately from `/api/status` (billing is revenue data and a heavier aggregate; it
must not touch the hot status path). Reads the rollup only. Returns:
`{ month, days_in_month, days_elapsed, provisioned_screens, billable_screens,
billable_screens_final?, is_final, tier, rate_usd, cost_usd, daily:[{day, active_screen_days}] }`.
**Month-to-date rule:** for the current month the average is computed over **completed
calendar days only** — today accrues live and appears in `daily` but is excluded from the
running average until it completes, so a partial today doesn't drag the estimate.
`billable_screens_final` and `is_final:true` appear only once the month is complete.
## First-full-month caveat
Metering starts accumulating **at deploy**. The first partial calendar month is incomplete
(and there is **no backfill**`device_status_log` only holds 3 days). **The first FULL,
clean billing month is the first whole calendar month after beta7 is deployed.**