crates/monetize-product/src/lib.rs
//! **The product as monetize serves it: its facts, and its estate over the pull channel.**
//!
//! monetize handles MANY products (gunnar.rs, holger.rs, ...) and knows none of them by name.
//! A product is two things here, and neither is a per-product adapter:
//!
//! * its **facts** ([`ProductFacts`]): its id, its meters, its words, its ticket and test
//! buttons, sent by the product's own signed guest and read once by the host;
//! * its **estate** ([`pulled::Pulled`]): the newest report of the estate's main
//! ([`pull::ProductReport`]) for every answer, and a signed order the main PULLS for every
//! change. monetize never calls into an estate. [`Served`] holds the two side by side.
//!
//! ```text
//! estate main ──report (pull channel)──▶ Pulled ──▶ monetize core
//! estate main ◀──signed order, pulled─── Pulled ◀── monetize core
//! ```
//!
//! # Two directions, both facts
//!
//! * [`pulled::Pulled::read_usage`]: what a tenant is consuming, in the product's own units
//! (gunnar: pack bytes, read-cache bytes, LFS bytes, tombstoned bytes, open-store RAM;
//! holger: whatever holger meters), read from the main's report as [`Usage`], a flat bag of
//! named meters, so core never has product-specific fields.
//! * [`pulled::Pulled::push_entitlement`]: the verdict, queued as a signed entitlement fact the
//! main pulls and appends to its attestation log.
//!
//! # Growth
//!
//! A box grows through the v2 pull path only: a paid step is a signed grow go-ahead the box
//! pulls, and the box asks `Fleet.Ask{grow_set}` to size its own data disks (monetize-server
//! `estate_order::sold::grow_own`, journaled by `monetize_cloud::grow`). An order past the room
//! is queued with [`pulled::Pulled::queue_room`] (a signed `grow_room` order). There is no
//! appliance grow drive here.
//!
//! # monetize never blocks the product
//!
//! Counting is done **inside the product, always, open source**: a self-hoster wants the
//! numbers too. monetize only *reads* them. If monetize is down the product keeps serving on
//! its cached entitlement; the next report and the next pulled order catch up.
use std::collections::BTreeMap;
/// The registration seam: how a product gets into a server without the server
/// knowing its name. A build registers what it links; the server looks ids up.
pub mod factory;
pub use factory::{body_secret_placeholder, GuestSpec, HttpAuth, ProductBuild, ProductFactory, PULLED_NEVER_DIALLED};
/// **Reading a daily `*_today` counter out of a series of readings** — the fold
/// behind the console's SILENT and FRICTION lists. Pure; see the module doc for
/// why summing the readings is the wrong arithmetic and why it lives here.
pub mod flow;
pub mod guardian;
pub mod pull;
pub mod pulled;
/// A tenant, as the product names it. gunnar: the `Namespace` name (`team/sub`).
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Default)]
pub struct TenantId(pub String);
/// **Whether the product will actually POLICE a cap on this meter.**
///
/// CPU and RAM exist as quota even where gunnar cannot cap them, and that gap is
/// narrowed without redoing the engine.
///
/// An order may cap any declared meter. But a cap on a meter nothing checks is a
/// promise nobody keeps, and the tenant cannot tell the difference from the
/// invoice. So the product says, per meter, which kind of promise it is — and
/// the console shows it beside the number rather than letting every cap look
/// like a guarantee.
///
/// This is the smallest honest way to let CPU and RAM be sold: they ARE bought,
/// with real money, and they DO change what the box can do — they are simply not
/// something a git server polices per namespace. Saying so is better than either
/// refusing to sell them or pretending they are enforced.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Enforcement {
/// **The product refuses when over.** A cap here is a wall the tenant hits.
/// gunnar: `pack_bytes` (receive-pack refused before the bytes),
/// `cache_bytes` (the explode budget).
Enforced,
/// **Counted and reported, not policed.** The number is true and monetize
/// can bill on it or a human can act on it, but nothing refuses. A cap here
/// is a threshold, not a wall.
Measured,
/// **Neither counted nor policed BY THE PRODUCT — it is iron monetize
/// bought.** CPU and RAM: a bigger plan at the provider, real money, real
/// effect, and no per-namespace check anywhere in the product. The
/// provider's invoice is the enforcement.
Provisioned,
}
impl Enforcement {
/// One line for the console, beside the cap.
pub fn describe(self) -> &'static str {
match self {
Enforcement::Enforced => "the product refuses when over",
Enforcement::Measured => "counted and reported; nothing refuses",
Enforcement::Provisioned => "provisioned at the cloud provider; not policed by the product",
}
}
/// Is a cap on this meter a wall the tenant will actually hit?
pub fn is_a_wall(self) -> bool {
matches!(self, Enforcement::Enforced)
}
}
/// **What a cap on this meter is MADE OF** — and therefore what UNIT the price
/// list prices it in and what iron, if any, the transaction buys for it.
///
/// Three of the four are capacity: bytes and cores, priced per GiB or core
/// per month, and (for disk) bought from a cloud. The fourth, [`Backing::Units`],
/// is a COUNT that is nevertheless SOLD — a property, a seat, a
/// mailbox — priced flat per unit per month and buying nothing. It exists
/// because a meter with no backing at all costs nothing to promise, and a
/// product whose whole price is a count (a per-property price) would
/// otherwise open a checkout for zero.
///
/// A plan that sells bytes it never buys is the one bug here that silently
/// converts money into nothing. Since the plan catalogue went (2026-09-05), an
/// ORDER names caps and nothing else, and this is the field that turns a cap
/// into a purchase: `monetize::order::resources_for` translates the DELTA on a
/// disk-backed meter into a `Resource::Disk`, and `monetize::PriceList` prices
/// each backing's delta by its own unit.
///
/// It lives on the meter and not on the order because only the PRODUCT knows
/// what its meter is made of — that `pack_bytes` is block storage and `pushes`
/// is a count of events that costs nothing to promise.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Backing {
/// **Bytes of block storage.** A delta here becomes a `Resource::Disk` of
/// `ceil(bytes / 2^30)` GiB, and is priced per GiB per month
/// (`disk_gib_month` on the price list).
DiskBytes,
/// **CPU, in millicores (1000 = one core).** Priced per core per month
/// (`cpu_core_month`). monetize does NOT translate millicores into a
/// provider's plan name (`2xCPU-4GB` is a name, and a table of names goes
/// stale every time a provider renames a size), so a delta here buys no
/// cloud resource of its own: the ceiling is the product's own measurement
/// (`Pulled::can_absorb`) and the box's declared limit
/// (`RoomLeft::ceilings`).
CpuMillicores,
/// **RAM, in bytes.** Priced per GiB per month (`ram_gib_month`); the same
/// ceiling rule as [`Backing::CpuMillicores`].
RamBytes,
/// **A counted, sold unit that is not iron** — a property register's
/// `fastigheter`, a seat, a mailbox. Priced flat per unit per month (`unit_month`, the
/// delta times the price, no rounding), and it buys NO cloud resource:
/// `resources_for` ignores it exactly as it ignores CPU and RAM. The
/// ceiling is the product's own answer (`Pulled::can_absorb`).
///
/// Without this a product priced per count had no way to be priced at all:
/// a meter with no backing "costs nothing to promise", which is right for
/// `pushes` and wrong for a property that is the whole subscription.
Units,
}
impl Backing {
/// The price-list key this backing is priced under, and the unit it means:
/// `disk_gib_month` | `cpu_core_month` | `ram_gib_month` | `unit_month`.
pub fn unit(self) -> &'static str {
match self {
Backing::DiskBytes => "disk_gib_month",
Backing::CpuMillicores => "cpu_core_month",
Backing::RamBytes => "ram_gib_month",
Backing::Units => "unit_month",
}
}
/// Every backing, in the order the price list is written in.
pub const ALL: [Backing; 4] = [Backing::DiskBytes, Backing::CpuMillicores, Backing::RamBytes, Backing::Units];
/// The backing a price-list key names, if any.
pub fn from_unit(unit: &str) -> Option<Backing> {
Backing::ALL.into_iter().find(|b| b.unit() == unit)
}
/// **Price `delta` of this backing at `minor_per_unit_month`, for one
/// month**, in minor units. Integer arithmetic, rounded UP to the unit for
/// bytes (a customer who asks for one byte over a GiB is sold the next GiB,
/// which is what the provider sells us), exact per millicore for CPU
/// (`minor × millicores / 1000`), and flat for a counted unit (`delta ×
/// minor` — five properties cost five times one).
pub fn price_month(self, delta: u64, minor_per_unit_month: u64) -> u64 {
const GIB: u64 = 1 << 30;
match self {
Backing::DiskBytes | Backing::RamBytes => delta.div_ceil(GIB).saturating_mul(minor_per_unit_month),
Backing::CpuMillicores => u64::try_from(u128::from(delta) * u128::from(minor_per_unit_month) / 1000).unwrap_or(u64::MAX),
Backing::Units => delta.saturating_mul(minor_per_unit_month),
}
}
}
/// **WHERE a meter's number is even meaningful: per tenant, or per product.**
///
/// Disk is the whole game, and it is per tenant. CPU and RAM stay in the model,
/// but for gunnar they are not interesting per user — they are interesting per
/// product: a tenant does not own cores, the box does.
///
/// Orthogonal to [`Enforcement`], and the pair is what makes a fleet total
/// honest. `Enforcement` says whether a cap is a WALL. Scope says whether adding
/// one tenant's figure to another's produces anything at all:
///
/// * `pack_bytes` is per tenant, so nine tenants' pack bytes sum to the fleet's
/// pack bytes;
/// * `cpu_millicores` is per product, so nine tenants' CPU does not sum to
/// anything — nobody measured a tenant's cores, and adding nine numbers that
/// were never measured is arithmetic on nothing.
///
/// **It is the PRODUCT's call, not a global rule.** gunnar meters disk per tenant
/// and CPU per product; another product may legitimately meter CPU per user, or
/// disk only in total. [`ProductFacts::meters`] is already that seam, and nothing
/// outside a product decides which of its meters are per-tenant. `monetize::fleet`
/// sums only [`Scope::Tenant`], whatever a product declares.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Scope {
/// **Measured, or sold, per TENANT.** A fleet figure is the sum over tenants.
/// The default, because it is what a meter usually is.
Tenant,
/// **Measured, or bought, for the WHOLE PRODUCT.** It appears ONCE, on the
/// product screen, and never in a per-tenant sum. A cap on it may still be
/// sold — CPU and RAM are bought with real money — it simply is not a
/// quantity any one tenant holds.
Product,
}
impl Scope {
/// May a figure for this meter be summed across tenants? Only [`Scope::Tenant`].
pub fn sums_across_tenants(self) -> bool {
matches!(self, Scope::Tenant)
}
/// `tenant` | `product` — the wire and JSON spelling, one writer.
pub fn name(self) -> &'static str {
match self {
Scope::Tenant => "tenant",
Scope::Product => "product",
}
}
}
/// **One meter a product declares**: its name, what it means, and whether a cap
/// on it is policed. An order may only cap a declared meter, and the console
/// shows [`Enforcement`] beside the number.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Meter {
/// The wire name, and the key in [`EntitlementFact::caps`] and [`Usage::meters`].
pub name: &'static str,
/// One line, the plugin's own words.
pub meaning: &'static str,
pub enforcement: Enforcement,
/// **Does [`pulled::Pulled::read_usage`] carry a reading for this meter?**
///
/// A meter exists to do one or both of two jobs: carry a usage READING, and
/// accept a CAP. Most do both. Two kinds do not, and conflating them was
/// caught by a test rather than by thinking:
///
/// * `cpu_millicores`, `ram_bytes` — the product has no idea what was
/// bought; monetize does, from the order. Reporting `0` would read as "you
/// have none", which is worse than saying nothing.
/// * `concurrent_transfers` — a cap the engine enforces instantly, with no
/// stored reading to bill from. The limit is real; the gauge does not exist.
///
/// So a plugin's `read_usage` must report EXACTLY the meters with this set,
/// which is an equality a test can hold in both directions: an undeclared
/// reading is a leak, and a declared reading that stopped arriving is a
/// silently emptied bill.
pub reports_usage: bool,
/// **What a cap on this meter is made of**, or `None` when a cap here costs
/// nothing to grant (a count of pushes, a number of repositories the
/// existing box already holds). `Some` is what makes an order on this meter
/// a PURCHASE: the delta is priced by the backing's unit and, for disk,
/// bought as iron. See [`Backing`].
pub backing: Option<Backing>,
/// **Whether this meter's number is per tenant or per product.** See
/// [`Scope`]; it is what keeps a fleet total from adding up figures nobody
/// measured per tenant.
pub scope: Scope,
}
impl Meter {
/// A meter that both reports a reading and may be capped — the common case.
pub const fn new(name: &'static str, meaning: &'static str, enforcement: Enforcement) -> Meter {
Meter { name, meaning, enforcement, reports_usage: true, backing: None, scope: Scope::Tenant }
}
/// A meter that may be CAPPED but carries no reading. See
/// [`Meter::reports_usage`] for the two kinds and why `0` is not a truthful
/// substitute.
pub const fn cap_only(name: &'static str, meaning: &'static str, enforcement: Enforcement) -> Meter {
Meter { name, meaning, enforcement, reports_usage: false, backing: None, scope: Scope::Tenant }
}
/// The same meter, declaring what a cap on it is MADE OF — and so what an
/// order on it buys and what the price list prices it by. A
/// [`Enforcement::Provisioned`] meter without one is a cap monetize can
/// neither price nor buy; gunnar's own test holds its list to that.
pub const fn with_backing(self, backing: Backing) -> Meter {
Meter { backing: Some(backing), ..self }
}
/// The same meter, measured for the WHOLE PRODUCT rather than per tenant —
/// so a fleet total leaves it out instead of adding up figures nobody took.
/// See [`Scope`].
pub const fn per_product(self) -> Meter {
Meter { scope: Scope::Product, ..self }
}
}
/// **What a product says it could still serve, or why it could not say.**
///
/// The other half of the oversell number: `sum(what has been sold) − servable`.
/// Without it monetize could sell ten tenants 10 GiB each on a box with 55 GiB
/// servable and nothing would object until the sixth push.
///
/// The two arms are the distinction `BASE-MODEL.md` rule 3 turns on and the same
/// one [`pulled::Pulled::can_absorb`] already makes: **an unanswered capacity question
/// is a measurement that did not happen, not a full disk and not an empty one.**
/// The live gunnar.rs appliance predates gunnar's `Admin.Capacity` RPC and
/// answers [`Servable::Unmeasured`] today; a fleet total that turned that into a
/// zero would report every deployment as catastrophically oversold, and one that
/// turned it into infinity would report every deployment as fine. Neither is a
/// measurement.
/// **Can this product's store grow while it runs?** Carried on
/// [`Servable::Measured`] beside the bytes, because the two answer different
/// questions and an operator acts differently on each: a full box that can grow
/// wants a disk; a full box that is SEALED wants its set enlarged by an operator
/// maintenance operation (the volumes it already has, grown in place) or a fresh
/// install onto a larger one, and a disk bought for it is billed and invisible.
///
/// Each arm carries the PRODUCT's own remediation sentence (gunnar's
/// `remediation()`), so the console renders what the product said rather than a
/// paraphrase of it — `UI.md` screen 2's `⚠ cannot grow` badge holds exactly
/// that paragraph.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Growth {
/// Cannot gain capacity while it runs, and nothing on the machine can give
/// it any. `GROWTH_SEALED` on gunnar's wire.
Sealed(String),
/// The product will not grow the set itself, but an operator can, out of
/// band. `GROWTH_OPERATOR_ONLY`.
OperatorOnly(String),
/// The product could not tell. NOT a promise that it can grow.
Unknown(String),
/// **Grows at its next restart** once every member volume has been
/// enlarged to one equal size (gunnar's `GROWTH_AT_RESTART`, 2026-09-14).
/// Still no hot-plug: a disk ATTACHED to it is as invisible as under
/// `Sealed`; what helps is the outside actor enlarging the members
/// (`monetize_cloud::grow`).
AtRestart(String),
/// **Grows without a restart**: the product drains, unmounts and grows into
/// enlarged members while its control plane stays up (gunnar's
/// `GROWTH_AT_RUNTIME`).
AtRuntime(String),
}
impl Growth {
/// The wire word: `sealed` | `operator_only` | `unknown` | `at_restart` |
/// `at_runtime`.
pub fn name(&self) -> &'static str {
match self {
Growth::Sealed(_) => "sealed",
Growth::OperatorOnly(_) => "operator_only",
Growth::Unknown(_) => "unknown",
Growth::AtRestart(_) => "at_restart",
Growth::AtRuntime(_) => "at_runtime",
}
}
/// The product's own sentence about what to do.
pub fn detail(&self) -> &str {
match self {
Growth::Sealed(s) | Growth::OperatorOnly(s) | Growth::Unknown(s) | Growth::AtRestart(s) | Growth::AtRuntime(s) => s,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Servable {
/// The product measured its own store.
Measured {
/// **Bytes that may still be handed to tenants**, beyond what they already
/// hold, after whatever reserve the product keeps for itself. gunnar's
/// `servable_bytes` (`free − disk floor`) — never its `free_bytes`, which
/// includes a reserve monetize must not sell.
servable_bytes: u64,
/// The whole store, reserve and all. `used + servable_bytes` is what the
/// fleet could grow to; `total_bytes` is larger than that by the reserve
/// and by anything on the filesystem that is not tenant data. 0 when the
/// product measures what is left but not what there is in total.
total_bytes: u64,
/// **Whether the store can EVER hold more than `total_bytes`.** A
/// measurement of the box, independent of the bytes: gunnar's
/// `Admin.Capacity` answers it as `growth`, and it is the field that
/// stops "buy another disk" being the reflex answer to a full fleet.
growth: Growth,
/// **The box's MEASURED ceilings on product-scope meters**, by the
/// product's own meter name: gunnar's `cpu_millicores_total` and
/// `ram_bytes_total` (Admin.Capacity, since gunnar 744bf72d) land here
/// as `cpu_millicores` / `ram_bytes`. A meter ABSENT here was not
/// measured — the wire spells that as an absent field, never as 0, and
/// a reader must never take a 0 as a ceiling. An order that would
/// raise a product-scope cap past a number here is refused by name;
/// the box's DECLARED ceilings (`RoomLeft::ceilings`) carry only
/// for a meter this map does not hold.
ceilings: BTreeMap<String, u64>,
},
/// **It could not be asked, or would not say — which is not zero.** The
/// string names WHICH: an RPC the deployed build predates, a control plane
/// that is down, a plugin that was never wired.
Unmeasured(String),
}
/// One named meter. Names are the product's, documented in its plugin crate, e.g.
/// `pack_bytes`, `cache_bytes`, `lfs_bytes`, `tombstoned_bytes`, `open_store_ram_bytes`,
/// `pushes`, `anonymous_reads`. Values are the product's units, usually bytes or counts.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Usage {
pub meters: BTreeMap<String, u64>,
pub measured_at_unix_ms: u64,
}
/// **One page of a BATCHED usage read** — [`pulled::Pulled::read_usage_page`]'s
/// question (2026-10-06).
///
/// The poller used to ask [`pulled::Pulled::read_usage`] once per tenant per tick: at
/// 1,000 tenants that was ~25–30 s of 1.4 cores on gunnar's main every five
/// minutes, linear in tenants, and at ~10,000 the whole period. A page answers
/// many tenants in ONE call, and `changed_since` lets the product answer only
/// the tenants whose usage moved — so a quiet estate costs a few calls, not one
/// per tenant.
///
/// **Who a listed tenant IS** ([`pulled::Pulled::tenant_owners`], 2026-10-08): the product's own
/// word for the namespace's owner, so a SELLING shop can give every PERSON's account a fact
/// (no account there is "no fact = uncapped") and leave the SELLER's own alone.
///
/// Every field defaults, and neither side denies unknown fields.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct TenantOwner {
pub tenant: TenantId,
/// The tenant is a PERSON's own account (gunnar: `owner_kind` USER). False for a group,
/// a clan, a machine's, or a namespace of no principal.
pub person: bool,
/// The owner holds a SERVER-scope grant at the product (the operator — the seller): the
/// shop's own, never a customer. Only the product says so; nothing a customer does sets it.
pub seller: bool,
}
/// Every field defaults, and neither side denies unknown fields: an older and a
/// newer build of either side read each other's JSON.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct UsagePageReq {
/// `""` asks for the first page; otherwise the previous page's
/// [`UsagePage::next_page_token`], unchanged.
#[serde(default)]
pub page_token: String,
/// At most this many rows; 0 = the product's own default.
#[serde(default)]
pub page_size: u32,
/// `""` = every tenant. Otherwise a [`UsagePage::cursor`] an earlier read
/// answered: only the tenants whose usage CHANGED since. A product that can
/// no longer honour it (it restarted, the day turned) answers every tenant
/// and says so with [`UsagePage::changed_only`] = false.
#[serde(default)]
pub changed_since: String,
}
/// One tenant's row in a [`UsagePage`].
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct TenantUsage {
pub tenant: TenantId,
pub usage: Usage,
}
/// **How long a whole keep boot may take** (audit 7, 2026-10-18; lane G9, A7 row 9): the keep
/// medium imported and cloned, the box stopped, the medium's run (the provider's own budget,
/// 45 min at UpCloud), the clone given back and the box started. The shop holds its Upgrade
/// call this long (`monetize_cloud::lay::KEEP_BOOT_WITHIN`), and every product's front waits on
/// it exactly as long (`monetize_embed::fleet::KEEP_BOOT_WITHIN`, Gunnar b74ca801): a front
/// that gave up first would report a failure while the shop still works. ONE constant, here,
/// because the shop's cloud crate and the fronts' embed crate both stand on this one.
pub const KEEP_BOOT_WITHIN: std::time::Duration = std::time::Duration::from_secs(120 * 60);
/// **The mark of a sold estate's tenant on the ONE ledger** (5.4, ONE monetize, G16): a
/// private Gunnar runs no monetize of its own, so its tenants are accounts of product `gunnar`
/// in the ONE ledger as `@<estate>/<namespace>`. `@` is no character of a gunnar namespace
/// (`gunnar-core` sandbox rules), so no tenant of the product's own estate reads as one.
pub const ESTATE_MARK: char = '@';
impl TenantId {
/// `namespace` on the sold estate `estate`, as the ONE ledger keys it: `@<estate>/<namespace>`.
pub fn on_estate(estate: &str, namespace: &str) -> Self {
TenantId(format!("{ESTATE_MARK}{estate}/{namespace}"))
}
/// `(estate, namespace)` when this is a sold estate's tenant ([`Self::on_estate`]); `None`
/// for a tenant of the product's own estate.
pub fn estate(&self) -> Option<(&str, &str)> {
let (estate, namespace) = self.0.strip_prefix(ESTATE_MARK)?.split_once('/')?;
(!estate.is_empty() && !namespace.is_empty()).then_some((estate, namespace))
}
}
/// **One page of a batched usage read** — [`pulled::Pulled::read_usage_page`]'s
/// answer. See [`UsagePageReq`].
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct UsagePage {
/// The readings, in the product's order. MAY include names that
/// [`pulled::Pulled::list_tenants`] does not list (a product's own machine
/// accounts); a caller keys by tenant and ignores the rest.
#[serde(default)]
pub rows: Vec<TenantUsage>,
/// `""` = this was the last page.
#[serde(default)]
pub next_page_token: String,
/// What to pass as [`UsagePageReq::changed_since`] next tick. The same on
/// every page of one read: it is taken when the read BEGAN, so a change that
/// lands while the pages are fetched is answered again next time rather than
/// lost.
#[serde(default)]
pub cursor: String,
/// True when `changed_since` was honoured and `rows` holds only the tenants
/// that changed; false when every tenant is in the read.
#[serde(default)]
pub changed_only: bool,
/// When the product took the reading this page answers from.
#[serde(default)]
pub as_of_unix_ms: u64,
}
/// **One tenant's FLOW for one UTC day** — what MOVED, as against what is held.
///
/// Every number in [`Usage`] is a STOCK reading: it answers *how much is on the
/// product's disk*, and it reads exactly the same for a customer who worked
/// this morning and one who last touched the product in March. monetize could
/// therefore bill nine tenants correctly and not know that six of them had
/// stopped. This is the other kind, and it is what the SILENT and FRICTION
/// lists are made of.
///
/// **The meters are the product's own names**, exactly as [`Usage::meters`] is,
/// and for the same reason: core has never held a product-specific field and
/// must not gain one here. gunnar fills `bytes_in`, `bytes_out`, `pushes`,
/// `fetches`, `lfs_bytes_in`, `lfs_bytes_out`, `refused`, its three
/// `refused_*` reasons, `auth_failures`, `distinct_principals` and the two
/// `last_*_unix_ms` timestamps. Another product will fill something else, and
/// nothing here needs to know.
///
/// **A day with no traffic has NO ROW.** Not a row of zeroes: the caller knows
/// which days it asked for, and zero-filling would put a bar of height zero
/// beside a bar that means *not counted*.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct ActivityDay {
/// Midnight UTC of the day, in Unix milliseconds.
pub day_unix_ms: u64,
/// The product's own meter names. See the type's doc.
pub meters: BTreeMap<String, u64>,
}
/// **A tenant's kept activity, and how far back the product remembers.**
///
/// The second half is not decoration. An empty `days` means one of two opposite
/// things — *nothing happened* or *you asked about a time the product no longer
/// remembers* — and only `retained_days` can tell them apart. A console that
/// drew an empty series as "silent" without it would report every tenant on a
/// freshly installed box as a lapsed customer.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Activity {
/// Oldest first. Days with no traffic are absent.
pub days: Vec<ActivityDay>,
/// How many days the product keeps. `0` when it would not say.
pub retained_days: u64,
}
/// The verdict monetize pushes back. Product-agnostic; the plugin maps it to the
/// product's own enum (gunnar: `EntitlementState`).
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct EntitlementFact {
pub tenant: TenantId,
/// **The ORDER this verdict came from** — the ledger reference
/// (`<product>/<tenant>/<date>+<caps>`) for a purchase, an operator's own
/// label (or nothing) for a hand-set fact. The field is still called `plan`
/// on the wire and in the signed canonical form (`monetize_embed::signing`),
/// because gunnar verifies that form and a renamed field would invalidate
/// every signature a deployed gunnar checks; the CONTENT is an order
/// reference since the plan catalogue went on 2026-09-05.
pub plan: String,
pub state: State,
pub paid_until_unix_ms: Option<u64>,
/// Per-meter caps the product enforces itself (gunnar: `pack_quota_bytes`,
/// `explode_budget_bytes`). Absent = product default.
pub caps: BTreeMap<String, u64>,
/// `operator` | `payment:<vendor>:<reference>` — lands in the product's attestation log.
pub source: String,
/// Ed25519 over the canonical JSON of the fields above, by monetize-server's key.
/// **The V1 form** — it does NOT cover [`EntitlementFact::issued_unix_ms`], and
/// that is deliberate: it is the signature an appliance built before
/// 2026-09-17 computes, and it must keep verifying there for ever. See
/// [`Self::issued_signature`].
pub signature: Vec<u8>,
/// **When monetize issued this verdict — the field that makes a fact good
/// ONCE.**
///
/// Without it an `EntitlementFact` is replayable for ever: capture a `Paid`
/// fact, wait for the tenant to lapse, push the captured bytes back, and the
/// signature still verifies because it is a real signature. `paid_until`
/// cannot tell the two apart — a lapse keeps the date and moves the ladder —
/// so the only thing that can is something MONOTONIC inside the signed form.
/// A product refuses a fact that is not newer than the one it holds
/// (gunnar: `EntitlementSlot::set`).
///
/// Unix milliseconds, and not a sequence number, for the same reason
/// [`crate`]'s sibling [`monetize_embed::signing::Snapshot`] chose one: a
/// clock needs no durable per-tenant counter on monetize's side, so a
/// restored ledger cannot rewind one and mint facts every appliance in the
/// field then refuses for ever. One idea in this system, not two.
///
/// `None` is a fact signed before this field existed. It stays legal, and a
/// product accepts it — until that product has seen ONE stamped fact for the
/// tenant, after which the unstamped form is a downgrade and is refused.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issued_unix_ms: Option<u64>,
/// **The V2 signature**: Ed25519 over the canonical JSON of every field
/// above INCLUDING `issued_unix_ms`
/// (`monetize_embed::signing::fact_message_issued`).
///
/// Two signatures and not one, because a fact has to be readable by two
/// generations of appliance at once. An appliance that predates this field
/// reads only fields 1-7 off the wire, computes the V1 form, and checks
/// [`Self::signature`] — so it accepts a stamped fact unchanged, and a
/// paying customer on an un-upgraded box loses nothing. An appliance that
/// knows the field checks BOTH, so the issue time is signed and cannot be
/// added, moved or bumped by whoever relays the fact.
///
/// Empty exactly when `issued_unix_ms` is `None`; neither is legal without
/// the other.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub issued_signature: Vec<u8>,
}
/// The ladder. Numbers (grace/retention days) are the deployment's policy
/// (`monetize::Policy`), not the enum's.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum State {
Free,
Paid,
Grace,
Suspended,
Retention,
}
#[derive(Clone, Debug)]
pub enum ProductError {
/// The product's control plane refused (auth, unknown tenant).
Refused(String),
/// Product unreachable. Retryable; nothing was written.
Unavailable(String),
}
impl std::fmt::Display for ProductError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProductError::Refused(r) => write!(f, "product refused: {r}"),
ProductError::Unavailable(r) => write!(f, "product unavailable: {r}"),
}
}
}
impl std::error::Error for ProductError {}
/// **Where the room for a target comes from, as the product measured it** —
/// what [`pulled::Pulled::can_absorb`] answers when it can serve.
///
/// A yes is two different facts, and the difference is money. gunnar's
/// `Admin.Capacity` answers a `verdict` and a `growth`, and the three readings
/// of them a seller can act on are: there is room already (buy nothing), there
/// is no room but a bought disk will be used (buy, and somebody has to extend a
/// filesystem), and there is no room and a bought disk will NOT be used (refuse).
/// The third is a refusal and never reaches this type. Before this type existed
/// a yes was `Ok(())` and the cloud was asked to `ensure` a disk regardless. With
/// the front's configuration (`pool_reserve_gib` covering the whole data set)
/// UpCloud's pool arithmetic answers "short" for every order, so every yes
/// bought and attached a disk to an appliance that had just said it had room —
/// read from the code and the config, not from a bill.
///
/// The transaction turns it into `monetize_cloud::Room` for the provider, so
/// the cloud is told what the product measured and never has to know which
/// product that was.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Absorb {
/// **Served out of what the product already has.** Nothing may be bought
/// for it: a disk bought on top of this answer is billed and not needed.
OnHand,
/// **Short today, and bought iron becomes usable** — by the hand `note`
/// names, in the product's own words (gunnar: an operator extends the
/// filesystem out of band, and gunnar measures the larger one on its next
/// look). Carried onto the purchase row so the ledger records that
/// somebody still has a job to do after the disk is attached.
WithIron { note: String },
}
/// **What a metered verb answered** — see [`pulled::Pulled::metered_door`]. Three
/// arms, because "nobody could ask" is neither a paid build nor a community
/// one, and a deployment that sells against any of the last two is selling
/// something it cannot deliver.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum MeteredDoor {
/// **The box answered a metered verb.** It has the paid services compiled
/// in; the string is whatever the plugin wants to record about the proof
/// (the verb it called, the build it saw).
Enforcing(String),
/// **The box answered `UNIMPLEMENTED`.** A community build. Every paid
/// signup against it will take the money and fail to grant the
/// entitlement, so a deployment that sells is misconfigured, loudly.
Absent(String),
/// **Nobody could ask** — the box is down, the plugin has no probe, nothing
/// has run yet. NOT a yes and NOT a no; carries which.
Unproven(String),
}
impl MeteredDoor {
/// `enforcing` | `absent` | `unproven` — the wire, JSON and log spelling,
/// one writer.
pub fn name(&self) -> &'static str {
match self {
MeteredDoor::Enforcing(_) => "enforcing",
MeteredDoor::Absent(_) => "absent",
MeteredDoor::Unproven(_) => "unproven",
}
}
/// The plugin's own words about the answer.
pub fn detail(&self) -> &str {
match self {
MeteredDoor::Enforcing(s) | MeteredDoor::Absent(s) | MeteredDoor::Unproven(s) => s,
}
}
/// **Has the paid door been PROVEN to be there?** False for `Unproven`,
/// which is the whole point of the arm: a deployment may not call itself
/// healthy on the strength of a question nobody answered.
pub fn proven(&self) -> bool {
matches!(self, MeteredDoor::Enforcing(_))
}
}
/// **One TEST BUTTON a product declares** (owner 2026-10-11): the operator's
/// console renders one button per declared action and monetize runs it through
/// the SAME sign-up / estate-order path a customer takes, on a vendor in test
/// mode only. Data, never markup: the console stays a thin renderer and the
/// product never ships HTML into it.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TestAction {
/// Stable within the product (`hosted-10gb`); what the console sends back.
pub id: String,
/// What the button reads (`Test: 10 GB hosted`).
pub label: String,
pub kind: TestActionKind,
}
/// **What a test button buys** — the two things a stranger can buy today.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TestActionKind {
/// A hosted account with this much room, at the plan the sign-up page
/// offers for it NOW (`Signup.Plans`) — the product names the room, never a
/// priced id, because the price is the shop's.
HostedPlan { cap_gib: u64 },
/// A private instance from the estate-order ladder, at these sizes (empty:
/// the ladder's own default for that field).
PrivateInstance {
#[serde(default)]
front_plan: String,
#[serde(default)]
main_plan: String,
#[serde(default)]
main_data_gib: u64,
},
}
/// **What is fixed of a product, as DATA** (A32-13): its manifest's id and meters and its facts'
/// tickets, test buttons and guardian. The host reads it ONCE ([`pulled::Pulled::facts`]) and holds it
/// in the product's slot, as the Cloud and Payment slots hold their manifests; every reader reads
/// the slot ([`Served`]), never the product.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ProductFacts {
/// `gunnar`, `holger`: the block's id. Also the first segment of every payment reference.
pub id: &'static str,
/// The meters this product declares — the UI and the order form read this, so an order
/// can only cap a meter that exists, and the console can say whether a cap on it is
/// policed ([`Enforcement`]).
pub meters: &'static [Meter],
/// **Its main mints actor tickets** (the facts' `tickets`): a console may offer
/// self-service renewal. False means there is nothing that could tell a customer from a
/// stranger, so the route must not exist rather than existing and refusing. The HOST checks
/// each ticket. Reported on the wire as `ProductInfo.tenant_renewal`.
pub tickets: bool,
/// **The test buttons this product offers its operator** ([`TestAction`]). Empty: the
/// console shows none for it.
pub test_actions: Vec<TestAction>,
/// **It brings an estate guardian** ([`guardian::EstateGuardian`], 5.4): how its estates are
/// diagnosed and repaired. The host holds the guardian beside the slot.
pub guardian: bool,
/// `Some(why)`: the facts could not be read yet (a guest that could not answer); its
/// tickets, test buttons and guardian are off, and the host asks again on the next look.
pub unread: Option<String>,
}
impl ProductFacts {
/// A product `id` with `meters` and nothing else: no tickets, no buttons, no guardian.
pub fn new(id: &'static str, meters: &'static [Meter]) -> ProductFacts {
ProductFacts { id, meters, ..Default::default() }
}
/// **These facts, interned**: one `'static` copy per distinct answer (the meter tables are
/// interned the same way), so a slot hands out `&'static` facts that outlive a replaced
/// guest, and reading the same answer again leaks nothing.
pub fn leak(self) -> &'static ProductFacts {
static HELD: std::sync::Mutex<Vec<&'static ProductFacts>> = std::sync::Mutex::new(Vec::new());
let mut held = HELD.lock().unwrap_or_else(|e| e.into_inner());
if let Some(f) = held.iter().find(|f| ***f == self) {
return f;
}
let f: &'static ProductFacts = Box::leak(Box::new(self));
held.push(f);
f
}
}
/// **A product as the host serves it**: its facts, read once, beside the product's estate verbs.
/// Copy, like `monetize_pay::Vendor`: the slot's facts (`&'static`) and the product. Every
/// reader of an id, a meter table, the ticket fact or the test buttons reads them here; the
/// estate verbs are the product's ([`std::ops::Deref`]).
#[derive(Clone, Copy)]
pub struct Served<'a> {
facts: &'a ProductFacts,
product: &'a pulled::Pulled,
guardian: Option<&'a dyn guardian::EstateGuardian>,
/// **The room a quote sells against**, when the host decides it (the queue past the room,
/// monetize-server `grow::can_absorb_queued`); `None`: the product's own answer.
absorb: Option<AbsorbFn<'a>>,
}
/// The host's own answer to a quote's room ([`Served::with_room`]).
pub type AbsorbFn<'a> = &'a (dyn Fn(&TenantId, &BTreeMap<String, u64>) -> Result<Absorb, String> + Sync);
impl<'a> Served<'a> {
/// `product` with the facts the host holds for it, and no guardian.
pub fn new(facts: &'a ProductFacts, product: &'a pulled::Pulled) -> Served<'a> {
Served { facts, product, guardian: None, absorb: None }
}
/// **The same, the quote's room decided by the host** (MONETIZE-FIX step 3, replacing the
/// `Growing` wrapper): `absorb` answers whether an order fits.
pub fn with_room(mut self, absorb: AbsorbFn<'a>) -> Served<'a> {
self.absorb = Some(absorb);
self
}
/// **Whether `caps` fit for `tenant`**: the host's room when it decides it, else the product's.
pub fn can_absorb(&self, tenant: &TenantId, caps: &BTreeMap<String, u64>) -> Result<Absorb, String> {
match self.absorb {
Some(f) => f(tenant, caps),
None => self.product.can_absorb(tenant, caps),
}
}
/// The same, with the estate guardian the host holds beside it.
pub fn with_guardian(mut self, guardian: Option<&'a dyn guardian::EstateGuardian>) -> Served<'a> {
self.guardian = guardian;
self
}
/// **The estate guardian that answers for it** (5.4): how its estates are diagnosed and
/// repaired. `None`: monetize repairs none of its estates.
pub fn guardian(&self) -> Option<&'a dyn guardian::EstateGuardian> {
self.guardian
}
/// The block's id.
pub fn id(&self) -> &'static str {
self.facts.id
}
/// The declared meters.
pub fn meters(&self) -> &'static [Meter] {
self.facts.meters
}
/// Its main mints actor tickets ([`ProductFacts::tickets`]).
pub fn tenant_renewal(&self) -> bool {
self.facts.tickets
}
/// Its operator's test buttons.
pub fn test_actions(&self) -> &'a [TestAction] {
&self.facts.test_actions
}
/// Everything the host holds of it.
pub fn facts(&self) -> &'a ProductFacts {
self.facts
}
/// The product's estate verbs, without the facts.
pub fn product(&self) -> &'a pulled::Pulled {
self.product
}
}
impl<'a> std::ops::Deref for Served<'a> {
type Target = pulled::Pulled;
fn deref(&self) -> &Self::Target {
self.product
}
}
impl std::fmt::Debug for Served<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Served").field("id", &self.facts.id).finish()
}
}
/// **What the host says about a ticket somebody presented** (`monetize_server::site_ticket`).
///
/// Not a `bool` and not a `Result<(), ProductError>`: a refusal is a normal,
/// expected answer that carries a SENTENCE — the words a caller may show — and
/// a `ProductError` means something else entirely (the product's control plane
/// is down, the question could not be asked). Conflating the two would let an
/// unreachable appliance read as a refused ticket, or worse, the reverse.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ActorVerdict {
/// The ticket verifies, names this tenant, this product and this verb, is
/// inside its life, and has not been spent before.
Allowed,
/// Refused, and why — in words a customer surface may show. A verifier must
/// not name the ticket's own tenant back to whoever asked about another
/// one; see `monetize_embed::verify_ticket`.
Refused(String),
}
/// **"Add users"** (owner, 2026-10-04): `count` users, each holding
/// `room_bytes` of the tenant's room. `count` 1 is ONE named user (`name`);
/// more is that many SEATS with no names, filled later (an invite, an
/// import). `room_bytes` 0 is the product's own default. The rules a person
/// types against (the range, the names, the room's floor) are the PRODUCT's,
/// which every door shares.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct UsersAsk {
pub count: u32,
pub room_bytes: u64,
#[serde(skip_serializing_if = "String::is_empty")]
pub name: String,
/// Where the named user's invite is mailed; empty: nowhere. Not part of
/// the ask's identity ([`UsersAsk::fingerprint`]).
#[serde(skip_serializing_if = "String::is_empty")]
pub email: String,
/// The console the invite is redeemed at.
#[serde(skip_serializing_if = "String::is_empty")]
pub invite_origin: String,
/// **The named user as the ledger knows them** (G13, 5.4): an opaque id
/// (`monetize::ledger::principal_id`), never the name. Empty for seats.
#[serde(skip_serializing_if = "String::is_empty")]
pub principal_id: String,
}
impl UsersAsk {
/// **What a ticket for this ask covers** — the "caps" an `order` actor
/// ticket is minted over and compared against, so a ticket vouches for
/// exactly this number of users at exactly this room, and no other.
pub fn ticket_caps(&self) -> BTreeMap<String, u64> {
[("users".to_owned(), u64::from(self.count.max(1))), ("user_room_bytes".to_owned(), self.room_bytes)].into_iter().collect()
}
/// A short, stable fingerprint for a reference: `2x10737418240` or
/// `1x10737418240:p-<id>` — the named user by [`UsersAsk::principal_id`],
/// never by name (G13).
pub fn fingerprint(&self) -> String {
match self.principal_id.as_str() {
"" => format!("{}x{}", self.count.max(1), self.room_bytes),
id => format!("{}x{}:{id}", self.count.max(1), self.room_bytes),
}
}
/// **The ask as the ledger may hold it**: no name, no address — the
/// named user only by [`UsersAsk::principal_id`].
pub fn without_names(&self) -> UsersAsk {
UsersAsk { name: String::new(), email: String::new(), ..self.clone() }
}
/// One named user (the name, or its id, is known).
pub fn is_named(&self) -> bool {
!self.name.is_empty() || !self.principal_id.is_empty()
}
}
/// The product's answer to [`pulled::Pulled::users_fit`] / [`pulled::Pulled::add_users`].
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct UsersAnswer {
/// The named user, once created; empty for seats and for a dry run.
pub principal: String,
/// Bytes each user holds, as the product resolved it (its default applied).
pub room_bytes: u64,
/// What the ask needs of the tenant's room: count × room.
pub need_bytes: u64,
/// The tenant's room neither stored nor promised, before the ask. `None`:
/// nothing caps the tenant there.
pub unused_bytes: Option<u64>,
/// How much is missing; 0 = it fits.
pub shortfall_bytes: u64,
/// The tenant's seats after this: every seat, and the open ones.
pub seats: u32,
pub seats_open: u32,
/// Nothing was written: a dry run, or the retry of a granted order.
pub unchanged: bool,
/// The new named user's invite secret, on the answer that created the
/// user (and never again): what a `<console>/login?invite=` link carries.
#[serde(skip_serializing_if = "String::is_empty")]
pub invite_secret: String,
}
/// What [`pulled::Pulled::purge_tenant`] did.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Purged {
pub tenant: TenantId,
/// What the product removed, in its own words and its own units — "5 of 5
/// stores, 1.2 GiB". Free text because every product counts different
/// things and a shared schema would force each of them to lie a little.
pub detail: String,
/// Bytes the product says it reclaimed. `None` means it does not count
/// them, which is not the same as zero and must not be rendered as it.
pub bytes_reclaimed: Option<u64>,
/// **Is the tenant still locked out of the product?**
///
/// `true` is a real and expected outcome — an operator may have suspended
/// the account separately, and the purge correctly refuses to lift a
/// lockout it did not take — but it is also what a purge that could not
/// unlock reports, and either way the tenant cannot use what is left.
/// Carried so a super purge can say so rather than infer it.
pub still_locked: bool,
/// One line per thing the product could not remove. Non-empty means the
/// tenant's data is PARTIALLY there, and the caller must not go on to
/// destroy the iron it is sitting on.
pub failures: Vec<String>,
}
/// **Bytes in the largest unit that still leaves a digit before the decimal
/// point**, by integer arithmetic: `10.0 GiB`, `64.0 MiB`, `999 B`.
///
/// One writer, because there used to be three and every one of them carried the
/// same bug. Each divided by 1 GiB unconditionally and printed one decimal, so
/// anything under a gibibyte rendered `0.0 GiB` — and `0.0` beside a quantity is
/// read as *there is none*:
///
/// * the console (then a Plans page) showed a real 64 MiB
/// `pack_bytes` cap as `0.0 GiB`, which an operator reads as "no quota";
/// * the old catalogue validator said *"sells pack_bytes = 10.0 GiB but
/// its resources buy only 10.0 GiB — short 0.0 GiB"*, a sentence in which
/// every number is wrong in the direction of "nothing is the matter";
/// * `products/gunnar`'s capacity refusals said "short 0.0 GiB" for the same
/// reason.
///
/// It lives here because this is the crate that already owns the meter
/// vocabulary ([`Meter`], [`Backing::DiskBytes`]) and the only one every side
/// can depend on: core, the product plugins, and the browser console alike. It
/// pulls in nothing (this crate is `serde` and nothing else), so the wasm
/// bundle pays a few hundred bytes for a formatter it was carrying anyway.
///
/// Truncates, never rounds up: a reading must not appear to cross a cap it has
/// not crossed.
pub fn bytes(n: u64) -> String {
const KIB: u64 = 1 << 10;
const MIB: u64 = 1 << 20;
const GIB: u64 = 1 << 30;
const TIB: u64 = 1 << 40;
let (unit, per) = match n {
n if n >= TIB => ("TiB", TIB),
n if n >= GIB => ("GiB", GIB),
n if n >= MIB => ("MiB", MIB),
n if n >= KIB => ("KiB", KIB),
// Under a kibibyte there is nothing to scale to, and `0.0 KiB` would be
// the same lie one unit down. Bytes are exact and short.
n => return format!("{n} B"),
};
format!("{}.{} {unit}", n / per, ((n % per) * 10) / per)
}
#[cfg(test)]
mod backing_tests {
use super::Backing;
/// **A byte over the GiB is sold the next GiB, and a millicore is priced
/// as a thousandth of a core.** Distinct non-zero numbers: 2200 öre per
/// GiB-month, 90 GiB + 1 byte, 1500 millicores at 30 000.
#[test]
fn prices_round_bytes_up_to_the_unit_and_cpu_by_the_millicore() {
const GIB: u64 = 1 << 30;
assert_eq!(Backing::DiskBytes.price_month(90 * GIB, 2200), 198_000, "90 GiB × 22.00 SEK");
assert_eq!(Backing::DiskBytes.price_month(90 * GIB + 1, 2200), 200_200, "one byte over is the 91st GiB");
assert_eq!(Backing::DiskBytes.price_month(0, 2200), 0);
assert_eq!(Backing::RamBytes.price_month(3 * GIB, 700), 2100);
assert_eq!(Backing::CpuMillicores.price_month(1500, 30_000), 45_000, "1.5 cores at 300.00");
assert_eq!(Backing::CpuMillicores.price_month(1, 30_000), 30, "one millicore is not free and not a core");
// A counted unit is flat: five properties at 200.00 SEK-month are 1000.00,
// and one is 200.00 — no rounding, no thousandths. This is the arm a per-property
// subscription rides on; before it existed a property priced to zero.
assert_eq!(Backing::Units.price_month(5, 20_000), 100_000, "5 × 200.00");
assert_eq!(Backing::Units.price_month(1, 20_000), 20_000);
assert_eq!(Backing::Units.price_month(0, 20_000), 0);
assert_eq!(Backing::Units.unit(), "unit_month");
// A zero unit price is FREE, whatever the delta: the open-source list.
for b in Backing::ALL {
assert_eq!(b.price_month(u64::MAX / 4, 0), 0, "{b:?}");
}
// The unit names are the price list's keys, both ways.
for b in Backing::ALL {
assert_eq!(Backing::from_unit(b.unit()), Some(b));
}
assert_eq!(Backing::from_unit("moon_month"), None);
}
}
#[cfg(test)]
mod bytes_tests {
use super::bytes;
/// **A quantity that exists never renders as zero.** RED before this
/// function existed: `0.0 GiB` for every value under 2^30, in three separate
/// copies of the same six lines.
#[test]
fn only_a_genuine_zero_reads_as_zero() {
for n in [1u64, 512, 1 << 20, 67_108_864, (1 << 30) - 1] {
let s = bytes(n);
assert!(!s.starts_with("0.0 ") && !s.starts_with("0 "), "{n} bytes rendered as {s:?}, which reads as nothing");
}
assert_eq!(bytes(0), "0 B");
}
#[test]
fn the_unit_scales_and_the_value_truncates() {
assert_eq!(bytes(999), "999 B");
assert_eq!(bytes(1536), "1.5 KiB");
assert_eq!(bytes(67_108_864), "64.0 MiB");
assert_eq!(bytes((1 << 30) - 1), "1023.9 MiB", "truncates, never rounds up past the cap");
assert_eq!(bytes(10 << 30), "10.0 GiB");
assert_eq!(bytes(3 << 40), "3.0 TiB");
assert_eq!(bytes(u64::MAX), "16777215.9 TiB", "no overflow at the top of the range");
}
}
#[cfg(test)]
mod estate_tenant_tests {
use super::*;
/// ★ G16: a sold estate's tenant is `@<estate>/<namespace>` on the ONE ledger and reads
/// back whole (a namespace keeps its own slashes); a tenant of the product's own estate,
/// nested or not, never reads as one, and a half-mark is no estate.
#[test]
fn a_sold_estates_tenant_carries_the_estate_in_its_path_and_no_own_tenant_reads_as_one() {
let t = TenantId::on_estate("acme", "bob");
assert_eq!(t.0, "@acme/bob");
assert_eq!(t.estate(), Some(("acme", "bob")));
assert_eq!(TenantId::on_estate("acme", "team/sub").estate(), Some(("acme", "team/sub")));
for own in ["bob", "team/sub", "acme/bob", "", "@acme", "@/bob", "@acme/", "x@acme/bob"] {
assert_eq!(TenantId(own.into()).estate(), None, "{own:?}");
}
}
}