crates/monetize-server/src/services/usage.rs
use std::sync::Arc;
use monetize::fleet::Oversell;
use monetize::ledger::Holding;
use monetize::trend::{self, Trend};
use monetize_cloud::lay::{same_resource, LaidEstate};
use monetize_cloud::{Cloud, Owned};
use monetize_product::{Servable, TenantId};
use monetize_proto::usage_server::Usage;
use monetize_proto::{self as pb};
use tonic::{Request, Response, Status};
use super::{blocking, product_or_not_found};
use crate::convert::usage_to_pb;
use crate::estate_order::{phase, EstateRun};
use crate::state::AppState;
use crate::store::StoreError;
pub struct UsageService(pub Arc<AppState>);
fn store_err(e: StoreError) -> Status {
Status::internal(format!("usage store: {e}"))
}
/// **The window a rate is quoted over when the caller names none: 30 days.**
///
/// `UI.md` renders `Full in ~9 weeks (12 GiB/wk, last 30 d)` — the window is on
/// the screen beside the number, so it has to be a stated constant rather than
/// whatever the store happens to hold. A month is long enough that one push does
/// not own the answer and short enough that last quarter's habits do not.
pub const DEFAULT_WINDOW_MS: u64 = 30 * 24 * 60 * 60 * 1000;
/// **How many readings one trend fold will look at per tenant.** A 30-day window
/// at the default fifteen-minute tick is ~2 880 readings; the ceiling is above
/// that so the ordinary case is never silently truncated, and below
/// `UsageStore::HISTORY_CEILING` so a fleet fold over many tenants cannot be
/// asked to decode an unbounded amount.
const TREND_READINGS: usize = 4_000;
/// The window's start: `window_ms` back from now, with 0 meaning
/// [`DEFAULT_WINDOW_MS`]. Returned alongside so the response can echo what was
/// actually applied.
fn window(state: &AppState, window_ms: u64) -> (u64, u64) {
let window_ms = if window_ms == 0 {
DEFAULT_WINDOW_MS
} else {
window_ms
};
(state.now().saturating_sub(window_ms), window_ms)
}
/// One folded rate on the wire. **The `enough_history` arm is the whole point**:
/// it is how "I do not know" reaches the console as something other than a zero.
fn trend_to_pb(
t: Trend,
window_ms: u64,
measured_tenants: u64,
unmeasured_tenants: u64,
) -> pb::TrendResponse {
let base = pb::TrendResponse {
enough_history: false,
per_week: 0.0,
confidence: String::new(),
largest_step_share: 0.0,
readings: 0,
span_ms: 0,
needed_span_ms: trend::MIN_SPAN_MS,
window_ms,
first_at_unix_ms: 0,
last_at_unix_ms: 0,
measured_tenants,
unmeasured_tenants,
};
match t {
Trend::NotEnoughHistory(n) => pb::TrendResponse {
readings: n.readings as u64,
span_ms: n.span_ms,
needed_span_ms: n.needed_span_ms,
..base
},
Trend::Rate(r) => pb::TrendResponse {
enough_history: true,
per_week: r.per_week,
confidence: r.confidence().name().into(),
largest_step_share: r.largest_step_share(),
readings: r.readings as u64,
span_ms: r.span_ms,
first_at_unix_ms: r.first_at_unix_ms,
last_at_unix_ms: r.last_at_unix_ms,
..base
},
}
}
/// **One [`monetize_product::ActivityDay`] → one `ActivityDay` on the wire.**
/// The one writer of that mapping.
///
/// The meters go out sorted by name because `BTreeMap` iteration is, and a
/// repeated field whose order changed between two identical reads would make
/// every consumer's diff noisy for nothing.
fn activity_day_to_pb(day: &monetize_product::ActivityDay) -> pb::ActivityDay {
pb::ActivityDay {
day_unix_ms: day.day_unix_ms,
meters: day
.meters
.iter()
.map(|(name, value)| pb::Meter {
name: name.clone(),
value: *value,
})
.collect(),
}
}
#[tonic::async_trait]
impl Usage for UsageService {
async fn read(
&self,
req: Request<pb::ReadUsageRequest>,
) -> Result<Response<pb::ReadUsageResponse>, Status> {
let r = req.into_inner();
blocking(&self.0, move |s| {
product_or_not_found(s, &r.product)?;
let usage = s.usage.get(&r.product, &r.tenant).map_err(store_err)?;
Ok(Response::new(pb::ReadUsageResponse {
usage: usage.map(|u| usage_to_pb(&TenantId(r.tenant.clone()), &u)),
}))
})
.await
}
async fn read_all(
&self,
req: Request<pb::ReadAllUsageRequest>,
) -> Result<Response<pb::ReadAllUsageResponse>, Status> {
let r = req.into_inner();
blocking(&self.0, move |s| {
product_or_not_found(s, &r.product)?;
let all = s.usage.all(&r.product).map_err(store_err)?;
Ok(Response::new(pb::ReadAllUsageResponse {
usages: all.iter().map(|(t, u)| usage_to_pb(t, u)).collect(),
}))
})
.await
}
/// The kept series for one tenant, oldest first. See the proto for the
/// retention window and why a `since` older than it is an answer, not an
/// error; see [`crate::state::UsageStore`] for what prunes it.
async fn history(
&self,
req: Request<pb::UsageHistoryRequest>,
) -> Result<Response<pb::UsageHistoryResponse>, Status> {
let r = req.into_inner();
blocking(&self.0, move |s| {
product_or_not_found(s, &r.product)?;
if r.tenant.is_empty() {
return Err(Status::invalid_argument(
"history is per tenant: tenant is required",
));
}
let tenant = TenantId(r.tenant.clone());
let readings = s
.usage
.history(&r.product, &r.tenant, r.since_unix_ms, r.limit as usize)
.map_err(store_err)?;
let kept = s.usage.kept(&r.product, &r.tenant).map_err(store_err)?;
Ok(Response::new(pb::UsageHistoryResponse {
readings: readings.iter().map(|u| usage_to_pb(&tenant, u)).collect(),
kept,
}))
})
.await
}
/// **The rate.** `monetize::trend` over the readings this server already
/// holds — a fold, not a measurement (`BASE-MODEL.md` rule 5: the product
/// counts, monetize decides).
///
/// An empty `tenant` is the FLEET: every tenant's rate, added. A tenant whose
/// own series cannot support one is counted, never folded in as zero, so the
/// caller can tell a fleet figure that is a total from one that is a floor.
async fn trend(
&self,
req: Request<pb::TrendRequest>,
) -> Result<Response<pb::TrendResponse>, Status> {
let r = req.into_inner();
blocking(&self.0, move |s| {
product_or_not_found(s, &r.product)?;
if r.meter.is_empty() {
return Err(Status::invalid_argument(
"a rate is per meter: name one of the product's meters (Products.List). There is no default \
meter, because a rate quoted for a meter the caller did not name is a number nobody can check",
));
}
let (since, window_ms) = window(s, r.window_ms);
if !r.tenant.is_empty() {
let readings = s.usage.history(&r.product, &r.tenant, since, TREND_READINGS).map_err(store_err)?;
let t = monetize::trend::rate_of(&readings, &r.meter);
let measured = u64::from(matches!(t, Trend::Rate(_)));
return Ok(Response::new(trend_to_pb(t, window_ms, measured, 1 - measured)));
}
// The fleet. Every tenant the store knows of for this product — the
// same list `ReadAll` answers from, so the fleet a rate is quoted for
// is the fleet the console lists.
let mut rates = Vec::new();
let mut unmeasured = 0u64;
for (tenant, _) in s.usage.all(&r.product).map_err(store_err)? {
let readings = s.usage.history(&r.product, &tenant.0, since, TREND_READINGS).map_err(store_err)?;
match monetize::trend::rate_of(&readings, &r.meter) {
Trend::Rate(rate) => rates.push(rate),
// NOT a zero. A tenant monetize cannot measure the slope of
// is a hole in the fleet figure, and the count is how the
// console learns the figure is a floor.
Trend::NotEnoughHistory(_) => unmeasured += 1,
}
}
let measured = rates.len() as u64;
let t = match monetize::Rate::sum(&rates) {
Some(rate) => Trend::Rate(rate),
// Not one tenant could be measured: unknown, not a fleet at rest.
None => Trend::NotEnoughHistory(monetize::trend::NotEnoughHistory {
readings: 0,
span_ms: 0,
needed_span_ms: trend::MIN_SPAN_MS,
}),
};
Ok(Response::new(trend_to_pb(t, window_ms, measured, unmeasured)))
})
.await
}
/// **The fleet total and the oversell number.** `monetize::fleet` over the
/// stored readings, the stored facts, and the poller's CACHED capacity
/// answer.
///
/// The cache is the load-bearing choice: `Product::servable` is a call to the
/// product's control plane, and making it here would hang a Products page on
/// the slowest box in the estate — which is exactly what `UI.md`'s
/// reachability dot exists to avoid. See `crate::health`.
/// **The tenant's flow series, asked of the PRODUCT.**
///
/// A passthrough and not a fold, unlike [`Self::history`] and
/// [`Self::trend`] beside it. The product is the only side that counts flow
/// (`BASE-MODEL.md` rule 5: the product counts, monetize decides), and a
/// copy kept in the usage store would be a second series to go stale — so
/// this asks, every time, and FAILS when the product cannot answer.
///
/// Failing is the whole point of not defaulting to an empty series. The
/// console draws an empty series as *this customer has gone quiet*, and a
/// product that is merely unreachable must never be able to put a working
/// customer on the SILENT list. `monetize_product::Product::read_activity`'s
/// default refuses by name for the same reason.
///
/// It runs on the blocking pool like every other plugin call, and is bounded
/// by the plugin's own per-call deadline.
async fn activity(
&self,
req: Request<pb::ActivityRequest>,
) -> Result<Response<pb::ActivityResponse>, Status> {
let r = req.into_inner();
blocking(&self.0, move |s| {
let product = product_or_not_found(s, &r.product)?;
if r.tenant.is_empty() {
return Err(Status::invalid_argument(
"activity is per tenant: tenant is required. There is no fleet answer, because two tenants' pushes are not one tenant's habits",
));
}
let activity = product
.read_activity(&TenantId(r.tenant.clone()), r.since_unix_ms, r.until_unix_ms)
.map_err(super::product_err)?;
Ok(Response::new(pb::ActivityResponse {
days: activity.days.iter().map(activity_day_to_pb).collect(),
retained_days: activity.retained_days,
}))
})
.await
}
async fn fleet(
&self,
req: Request<pb::FleetRequest>,
) -> Result<Response<pb::FleetResponse>, Status> {
let r = req.into_inner();
blocking(&self.0, move |s| {
let product = product_or_not_found(s, &r.product)?;
let usages = s.usage.all(&r.product).map_err(store_err)?;
let facts = s
.ledger
.entitlements(&r.product)
.map_err(super::ledger_err)?;
let facts = crate::signup::with_signup_facts(s, &r.product, facts)
.map_err(|e| Status::internal(format!("signup store: {e}")))?;
let health = s.health.get(&r.product);
let servable = health.servable_or_unasked();
// Only the per-TENANT meters. A Scope::Product meter is not in the
// list at all — `monetize::fleet::totals` drops it, because there is
// nothing to add up.
let meters = monetize::fleet::totals(product.meters(), &usages, &facts, &servable)
.iter()
.map(fleet_meter_to_pb)
.collect();
let (
servable_measured,
servable_bytes,
total_bytes,
servable_reason,
growth,
growth_detail,
) = match &servable {
Servable::Measured {
servable_bytes,
total_bytes,
growth,
..
} => (
true,
*servable_bytes,
*total_bytes,
String::new(),
growth.name().to_owned(),
growth.detail().to_owned(),
),
// An unmeasured box has not said whether it can grow either:
// empty, not `unknown`, so a reader cannot mistake "nobody asked"
// for "it answered that it could not tell".
Servable::Unmeasured(why) => {
(false, 0, 0, why.clone(), String::new(), String::new())
}
};
Ok(Response::new(pb::FleetResponse {
meters,
servable_measured,
servable_bytes,
total_bytes,
servable_reason,
servable_at_unix_ms: health.servable_at_unix_ms,
growth,
growth_detail,
cloud: Some({
let mut c = cloud_inventory_pb(
&r.product,
s.registry.cloud_for_transactions(),
&s.ledger,
&EstateIron::of(s),
);
// MAP-3: the slot it was listed through names the account; every product's
// block from one slot is one account.
c.account = s
.registry
.transaction_slot()
.map(|h| h.name().to_owned())
.unwrap_or_default();
c
}),
}))
})
.await
}
}
/// **One estate order's laid iron**, as the CLOUD panel counts it OWNED (2026-10-22):
/// an estate order keeps what it bought in its `laid` record, never as ledger
/// holdings, so a panel reading the ledger alone called every shop-laid machine
/// an orphan. A run that is gone ([`phase::is_gone`]) or never laid owns nothing.
#[derive(Clone, Debug, PartialEq)]
pub struct LaidIron {
/// `estate/<company>` — the label every laid resource carries.
pub reference: String,
pub laid: LaidEstate,
/// The provider ids the give-back already asked to go: absent, never missing.
pub given_back: Vec<String>,
/// **The estate this iron belongs to** (MAP-SEG 1, 2026-10-26): the run's fleet name —
/// a site's label (`test2-gunnar-rs`), a sold estate's company — the name the console's
/// `/e/<estate>/` pages are addressed by. Empty: not known (a bare record).
pub estate: String,
/// **Adopted, not laid** (MAP-SEG 4): a kit-installed site the ONE box adopted
/// ([`EstateRun::adopted_at_ms`]). Its machines carry no label of this box (the kit made
/// them), so the provider's labelled listing never holds them; the inventory looks each
/// one up by the uuid the adoption read back ([`adopted_standing`]).
pub adopted: bool,
}
impl LaidIron {
pub fn of(run: &EstateRun) -> Option<Self> {
Self::from_parts(
&run.phase,
&run.reference,
run.laid.as_ref(),
&run.given_back,
)
.map(|l| l.named(run.fleet_name()).adopted_if(run.adopted_at_ms > 0))
}
pub fn from_parts(
phase_now: &str,
reference: &str,
laid: Option<&LaidEstate>,
given_back: &[String],
) -> Option<Self> {
if phase::is_gone(phase_now) {
return None;
}
Some(LaidIron {
reference: reference.to_owned(),
laid: laid?.clone(),
given_back: given_back.to_vec(),
estate: String::new(),
adopted: false,
})
}
/// The estate (its fleet name) this iron belongs to.
pub fn named(mut self, estate: &str) -> Self {
self.estate = estate.to_owned();
self
}
/// Marked adopted (kit-installed, not laid by this box) when `adopted`.
pub fn adopted_if(mut self, adopted: bool) -> Self {
self.adopted = adopted;
self
}
}
/// **Which estate a cloud row belongs to** (MAP-SEG 1, 2026-10-26: the Map of ONE estate
/// listed the iron of every estate on the account). By the row's reference: an estate
/// order's `estate/<company>` is that run's estate ([`LaidIron::estate`]; a run gone or
/// unknown here: its company, which is a sold estate's fleet name); a tenant's holding
/// `<product>/@<estate>/<ns>/…` is the sold estate `<estate>`. Anything else (a product's
/// own tenant, the box's own data set, an unlabelled row) is `""`: no ONE estate's, so it
/// shows on the operator's account-wide Iron page and on no estate's Map.
pub fn estate_of(reference: &str, estates: &[LaidIron]) -> String {
if reference.is_empty() {
return String::new();
}
if let Some(l) = estates.iter().find(|l| l.reference == reference) {
if !l.estate.is_empty() {
return l.estate.clone();
}
}
if let Some(company) = reference.strip_prefix("estate/") {
return company.split('/').next().unwrap_or("").to_owned();
}
reference
.split('/')
.nth(1)
.and_then(|t| t.strip_prefix('@'))
.map(|e| e.split('/').next().unwrap_or("").to_owned())
.unwrap_or_default()
}
/// **The estate orders' iron, for [`cloud_inventory_pb`]**: every laid, not-gone run,
/// and the product estates are sold as (`[estate_orders] product`) — the panel that
/// lists them as its own. `product: None` (nothing sold here) still passes the laid
/// rows, so no other product's panel calls them orphans.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct EstateIron {
pub product: Option<String>,
pub laid: Vec<LaidIron>,
}
impl EstateIron {
pub fn of(state: &AppState) -> Self {
EstateIron {
product: state.sold_product().ok().map(str::to_owned),
laid: state
.estates
.all()
.iter()
.filter_map(LaidIron::of)
.collect(),
}
}
}
/// **The laid records as ledger-shaped rows**, so the pure [`monetize::reconcile`]
/// can match them: each laid machine's server and every disk, under the run's
/// reference, with the id the provider LISTS it by (a laid id is bare, a listed one
/// may carry a kind prefix — [`same_resource`]). A laid id the provider no longer
/// lists keeps its bare form and comes out MISSING, unless the give-back already
/// asked it to go. A release clone counts only while it is listed: it is ejected
/// after hand-over, and its absence is no loss.
pub fn estate_holdings(provider: &str, listed: &[Owned], estates: &[LaidIron]) -> Vec<Holding> {
use monetize_cloud::{Resource, ResourceId, ServerId, ServerRole};
let mut out: Vec<Holding> = Vec::new();
for e in estates {
let given_back = |laid: &str| e.given_back.iter().any(|g| same_resource(g, laid));
let mut push = |laid: &str, resource: Resource, only_listed: bool| {
if laid.trim().is_empty() {
return;
}
let id = match listed
.iter()
.find(|o| o.id.provider == provider && same_resource(&o.id.id, laid))
{
Some(o) => o.id.clone(),
None if only_listed || given_back(laid) => return,
None => ResourceId {
provider: provider.to_owned(),
id: laid.to_owned(),
},
};
if out.iter().any(|h| h.id.as_ref() == Some(&id)) {
return;
}
out.push(Holding {
reference: e.reference.clone(),
resource,
id: Some(id),
});
};
// An adopted site's machines are compared like any laid ones (MAP-SEG 4): the
// inventory looked each up BY ITS UUID first ([`adopted_standing`]), so a machine that
// stands is listed and one really gone is MISSING.
let only_listed = false;
for m in &e.laid.machines {
let role = if m.role == "twin" {
ServerRole::Replica
} else {
ServerRole::Primary
};
push(
&m.server,
Resource::Server {
plan: m.plan.clone(),
zone: e.laid.zone.clone(),
role,
},
only_listed,
);
for d in &m.disks {
push(
&d.id,
Resource::Disk {
server: ServerId(m.server.clone()),
gib: d.gib,
},
only_listed,
);
}
push(
&m.medium_clone,
Resource::Disk {
server: ServerId(m.server.clone()),
gib: 0,
},
true,
);
}
}
out
}
/// **An adopted estate's machines, looked up BY UUID** (MAP-SEG 4, owner 2026-10-26: "3 MISSING
/// gunnarrs servers"). A kit-installed site's servers and disks carry no `monetize_ref` label, so
/// the labelled listing never holds them and every one read MISSING. Each one the listing does
/// not hold is asked for by the uuid its adoption read back (`Listing::Status`): one that
/// stands is a listed row under the run's reference (unpriced: nothing listed its price); one
/// the provider no longer has is not added, so it is MISSING; one the provider could not say
/// about is left out of the comparison ([`LaidIron`] cut), never called missing on a guess.
pub fn adopted_standing(
cloud: &dyn Cloud,
listed: &[Owned],
estates: &[LaidIron],
currency: monetize_pay::Currency,
) -> (Vec<Owned>, Vec<LaidIron>) {
use monetize_cloud::{CloudError, OwnedKind, ResourceId, ResourceStatus};
let named = cloud.manifest().provider;
let provider = named.as_str();
let mut found = Vec::new();
let mut cut = Vec::new();
for e in estates {
if !e.adopted {
cut.push(e.clone());
continue;
}
let mut e = e.clone();
let zone = e.laid.zone.0.clone();
let mut unknown: Vec<String> = Vec::new();
let mut ask = |bare: &str,
kind: OwnedKind,
gib: u64,
attached_to: Option<String>,
found: &mut Vec<Owned>| {
if bare.trim().is_empty()
|| listed
.iter()
.any(|o| o.id.provider == provider && same_resource(&o.id.id, bare))
{
return;
}
let prefix = if kind == OwnedKind::Server {
"server"
} else {
"storage"
};
let id = ResourceId {
provider: provider.to_owned(),
id: if bare.contains('/') {
bare.to_owned()
} else {
format!("{prefix}/{bare}")
},
};
match monetize_cloud::ask::status(&cloud, &id) {
Ok(ResourceStatus::Gone) | Err(CloudError::UnknownResource(_)) => {}
Ok(_) => found.push(Owned {
id,
kind,
gib,
zone: zone.clone(),
attached_to,
reference: Some(e.reference.clone()),
monthly_minor: None,
currency,
provenance: provider.to_owned(),
resource: None,
backup: None,
}),
Err(err) => {
eprintln!("monetize-server: the adopted {} {bare} could not be looked up ({err}): it is left out of the iron's comparison", e.reference);
unknown.push(bare.to_owned());
}
}
};
for m in &e.laid.machines {
ask(&m.server, OwnedKind::Server, 0, None, &mut found);
for d in &m.disks {
ask(
&d.id,
OwnedKind::Disk,
d.gib,
Some(m.server.clone()),
&mut found,
);
}
}
for m in &mut e.laid.machines {
m.disks.retain(|d| !unknown.contains(&d.id));
}
e.laid.machines.retain(|m| !unknown.contains(&m.server));
cut.push(e);
}
(found, cut)
}
/// **CLOUD: what we own, what it costs, orphans/missing** — for one product.
///
/// The provider is LISTED here, live, then reconciled against the WHOLE ledger
/// (`all_holdings`, not this product's slice): an orphan is a resource in no
/// product's ledger, and only the whole ledger can say that. The result is then
/// cut to `product`: its matched holdings and its missing ones — plus every
/// orphan, on every product, because an orphan belongs to none and must be seen.
///
/// The estate orders' laid iron ([`EstateIron`]) is reconciled with the ledger: a
/// shop-laid machine is OWNED under `estate/<company>` on the sold product's panel,
/// never an orphan. Nothing here cleans anything up: the list is for eyes.
///
/// A provider that cannot be listed answers `available: false` with the reason
/// and empty lists; the fleet's meters still render. Nobody asked is not
/// "nothing is there".
pub fn cloud_inventory_pb(
product: &str,
cloud: &dyn Cloud,
ledger: &monetize::Ledger,
estates: &EstateIron,
) -> pb::CloudInventory {
let provider = cloud.manifest().provider.to_owned();
let unavailable = |error: String| pb::CloudInventory {
provider: provider.clone(),
available: false,
error,
..Default::default()
};
let inv = match monetize_cloud::ask::account(&cloud) {
Ok(i) => i,
Err(e) => return unavailable(e.to_string()),
};
let mut all = match ledger.all_holdings() {
Ok(h) => h,
Err(e) => return unavailable(format!("ledger: {e}")),
};
let mut inv = inv;
let (standing, laid) = adopted_standing(cloud, &inv.owned, &estates.laid, inv.currency);
inv.owned.extend(standing);
// MAP-3: a backup is no order's disk; it is classified by its origin below.
let (backup_rows, rows): (Vec<Owned>, Vec<Owned>) = std::mem::take(&mut inv.owned)
.into_iter()
.partition(Owned::is_backup);
inv.owned = rows;
all.extend(estate_holdings(&provider, &inv.owned, &laid));
let rec = monetize::reconcile(&inv.owned, &all);
let prefix = format!("{product}/");
// The sold product's panel lists the estates it laid as its own.
let sells_estates = estates.product.as_deref() == Some(product);
let mine = |reference: &str| {
reference.starts_with(&prefix)
|| (sells_estates && estates.laid.iter().any(|l| l.reference == reference))
};
// What this product's orders bought, then the box's own iron — the `server`
// cloud's `dataset` row for THIS product, which no order bought and which
// the panel shows as the iron itself rather than as an orphan.
let owned: Vec<pb::OwnedResource> = rec
.matched
.iter()
.filter(|m| mine(&m.holding.reference))
.map(|m| {
owned_to_pb(
&m.owned,
&m.holding.reference,
&estate_of(&m.holding.reference, &estates.laid),
)
})
.chain(
rec.own
.iter()
.filter(|o| o.attached_to.as_deref() == Some(product))
.map(|o| owned_to_pb(o, "", "")),
)
.collect();
let mut orphans: Vec<pb::OwnedResource> = rec
.orphans
.iter()
.map(|o| {
let reference = o.reference.as_deref().unwrap_or("");
owned_to_pb(o, reference, &estate_of(reference, &estates.laid))
})
.collect();
let (backups, orphan_backups) = backups_of(&backup_rows, &rec, &estates.laid);
orphans.extend(orphan_backups);
let missing = rec
.missing
.iter()
.filter(|h| mine(&h.reference))
// `missing` only ever holds BOUGHT holdings (`reconcile` drops OnHand), so
// the id is always there; `filter_map` says so without an unwrap.
.filter_map(|h| {
let id = h.id.clone()?;
Some(pb::MissingResource {
provider: id.provider,
id: id.id,
reference: h.reference.clone(),
tenant: tenant_of(&h.reference),
resource: monetize_cloud::fingerprint(&h.resource),
estate: estate_of(&h.reference, &estates.laid),
})
})
.collect();
pb::CloudInventory {
provider,
available: true,
error: String::new(),
total_bytes: owned.iter().map(|o| o.gib << 30).sum(),
monthly_minor: owned
.iter()
.filter(|o| o.priced)
.map(|o| o.monthly_minor)
.sum(),
unpriced: owned.iter().filter(|o| !o.priced).count() as u64,
owned,
orphans,
missing,
foreign_count: inv.foreign,
// The provider's own word on what `monthly_minor` is a number of,
// spelled as `currency_name` spells `Money` to the console.
currency: inv.currency.code().to_owned(),
backup_bytes: backups.iter().map(|b| b.gib << 30).sum(),
backups,
// The linked cloud's name, set by the caller that knows the slot ([`Usage::fleet`]).
account: String::new(),
}
}
/// The two spellings of one storage (`storage/<uuid>` listed, `<uuid>` laid) are the same.
fn same_storage(a: &str, b: &str) -> bool {
same_resource(a, b) || same_resource(b, a)
}
/// **The account's backups, each by the disk it was taken of** (MAP-3, owner 2026-10-27):
/// a backup of a disk an order or an estate holds (`rec.matched`) is that estate's; a backup
/// of a disk that stands and is no order's (the ONE box's own disk) is the account's own, no
/// estate's; a backup whose disk is GONE, or is itself an orphan, is an orphan, by its title.
/// Answers (the backups, the orphan backups).
fn backups_of(
rows: &[Owned],
rec: &monetize::Reconciliation,
laid: &[LaidIron],
) -> (Vec<pb::BackupResource>, Vec<pb::OwnedResource>) {
let mut backups = Vec::new();
let mut orphans = Vec::new();
for b in rows {
let Some(of) = b.backup.as_ref() else {
continue;
};
let own_reference = b.reference.as_deref().unwrap_or("");
let disk_is_orphan = rec
.orphans
.iter()
.any(|o| same_storage(&o.id.id, &of.origin));
if !of.origin_standing || of.origin.is_empty() || disk_is_orphan {
orphans.push(owned_to_pb(
b,
own_reference,
&estate_of(own_reference, laid),
));
continue;
}
// The disk's holder, or none: a disk that stands and no order labelled.
let holder = rec
.matched
.iter()
.find(|m| same_storage(&m.owned.id.id, &of.origin))
.map(|m| m.holding.reference.as_str());
let estate = holder.map(|r| estate_of(r, laid)).unwrap_or_default();
let of_what = if !of.server.is_empty() {
of.server.as_str()
} else {
of.origin.as_str()
};
let day = of.created.get(..10).unwrap_or(of.created.as_str());
backups.push(pb::BackupResource {
provider: b.id.provider.clone(),
id: b.id.id.clone(),
gib: b.gib,
zone: b.zone.clone(),
origin: of.origin.clone(),
server: of.server.clone(),
created: of.created.clone(),
title: of.title.clone(),
estate,
reference: holder.unwrap_or(own_reference).to_owned(),
words: if day.is_empty() {
format!("backup of {of_what}")
} else {
format!("backup of {of_what}, {day}")
},
});
}
(backups, orphans)
}
/// The tenant is the reference's second segment (`<product>/<tenant>/<date>…`).
fn tenant_of(reference: &str) -> String {
reference.split('/').nth(1).unwrap_or("").to_owned()
}
fn owned_to_pb(o: &monetize_cloud::Owned, reference: &str, estate: &str) -> pb::OwnedResource {
pb::OwnedResource {
provider: o.id.provider.clone(),
id: o.id.id.clone(),
kind: if o.is_backup() {
"backup".to_owned()
} else {
o.kind.name().to_owned()
},
gib: o.gib,
zone: o.zone.clone(),
attached_to: o.attached_to.clone().unwrap_or_default(),
reference: reference.to_owned(),
tenant: tenant_of(reference),
priced: o.monthly_minor.is_some(),
monthly_minor: o.monthly_minor.unwrap_or(0),
provenance: o.provenance.clone(),
estate: estate.to_owned(),
resource: o.resource.clone().unwrap_or_default(),
title: o
.backup
.as_ref()
.map(|b| b.title.clone())
.unwrap_or_default(),
}
}
/// One fleet total on the wire. The `oversell` STRING is read before
/// `oversell_bytes`, because `unmeasured` and `not_storage` carry no bytes and a
/// reader that took the number alone would see a confident zero.
fn fleet_meter_to_pb(t: &monetize::FleetTotal) -> pb::FleetMeter {
let (oversell, oversell_bytes, oversell_reason) = match t.oversell() {
Oversell::Oversold { by } => ("oversold", by, String::new()),
Oversell::Fits { headroom } => ("fits", headroom, String::new()),
Oversell::Unmeasured(why) => ("unmeasured", 0, why),
Oversell::NotStorage => ("not_storage", 0, String::new()),
};
pb::FleetMeter {
meter: t.meter.clone(),
tenants: t.tenants as u64,
used: t.used,
tenants_without_reading: t.tenants_without_reading as u64,
allowance: t.allowance,
tenants_without_cap: t.tenants_without_cap as u64,
oversell: oversell.into(),
oversell_bytes,
oversell_reason,
capacity: t.capacity(),
is_a_floor: t.is_a_floor(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use monetize_proto::usage_server::Usage as _;
use monetize::testing::MockProduct;
use monetize_product::Usage;
use crate::registry::Registry;
const GIB: u64 = 1 << 30;
const DAY: u64 = 24 * 60 * 60 * 1000;
const NOW: u64 = 1_788_523_200_000;
fn state() -> Arc<AppState> {
let mut s = AppState::in_memory(Registry::new().product(MockProduct::gunnar()));
// A fixed clock, so the 30-day window lands on the readings the test wrote
// rather than on wherever today happens to be.
s.clock = Box::new(|| NOW);
Arc::new(s)
}
fn store(s: &AppState, tenant: &str, days: u64, mut value: impl FnMut(u64) -> u64) {
for d in 0..days {
let meters: BTreeMap<String, u64> = [("pack_bytes".to_string(), value(d))].into();
s.usage
.put(
"gunnar",
tenant,
&Usage {
meters,
measured_at_unix_ms: NOW - (days - 1 - d) * DAY,
},
)
.unwrap();
}
}
async fn ask(s: &Arc<AppState>, tenant: &str, meter: &str) -> pb::TrendResponse {
UsageService(s.clone())
.trend(Request::new(pb::TrendRequest {
product: "gunnar".into(),
tenant: tenant.into(),
meter: meter.into(),
window_ms: 0,
}))
.await
.expect("the product is linked and the meter is named")
.into_inner()
}
/// (r) **The RPC carries the three answers apart**: a measured tenant gets a
/// value, a tenant with an hour of history gets `enough_history = false`, and
/// a flat tenant gets a rate of ZERO with `enough_history = true`.
///
/// Asserted on the wire fields, because the whole risk of this endpoint is a
/// serialisation that flattens "unknown" and "zero" into the same `0.0`.
#[tokio::test]
async fn the_wire_keeps_unknown_and_zero_apart() {
let s = state();
store(&s, "growing", 28, |d| 10 * GIB + d * GIB / 7); // 1 GiB/wk
store(&s, "flat", 28, |_| 5 * GIB);
s.usage
.put(
"gunnar",
"fresh",
&Usage {
meters: [("pack_bytes".to_string(), GIB)].into(),
measured_at_unix_ms: NOW - 3_600_000,
},
)
.unwrap();
s.usage
.put(
"gunnar",
"fresh",
&Usage {
meters: [("pack_bytes".to_string(), 3 * GIB)].into(),
measured_at_unix_ms: NOW,
},
)
.unwrap();
let growing = ask(&s, "growing", "pack_bytes").await;
assert!(growing.enough_history);
assert!(
(growing.per_week / GIB as f64 - 1.0).abs() < 0.01,
"{} bytes/wk",
growing.per_week
);
assert_eq!(growing.confidence, "firm");
assert_eq!(
growing.window_ms, DEFAULT_WINDOW_MS,
"the window is echoed so the console can print 'last 30 d'"
);
let flat = ask(&s, "flat", "pack_bytes").await;
assert!(flat.enough_history, "a month of stillness IS history");
assert_eq!(flat.per_week, 0.0);
let fresh = ask(&s, "fresh", "pack_bytes").await;
assert!(!fresh.enough_history, "an hour apart is not a week");
assert_eq!(
fresh.per_week, 0.0,
"the field is empty, and `enough_history` is what says so"
);
assert_eq!(fresh.needed_span_ms, monetize::trend::MIN_SPAN_MS);
assert_eq!(fresh.span_ms, 3_600_000);
}
/// (r) **The fleet arm counts the tenants it could not measure and does not
/// fold them in as zero.** A fleet total that quietly treats an unmeasurable
/// tenant as flat is the arithmetic-on-nothing bug, and the count is the only
/// thing that lets a console say the figure is a floor.
#[tokio::test]
async fn the_fleet_arm_counts_what_it_could_not_measure() {
let s = state();
store(&s, "a", 28, |d| 10 * GIB + d * GIB / 7);
store(&s, "b", 28, |d| 10 * GIB + d * GIB / 7);
// Two tenants the poller has only just met.
for t in ["c", "d"] {
s.usage
.put(
"gunnar",
t,
&Usage {
meters: [("pack_bytes".to_string(), GIB)].into(),
measured_at_unix_ms: NOW,
},
)
.unwrap();
}
let fleet = ask(&s, "", "pack_bytes").await;
assert!(fleet.enough_history);
assert_eq!(fleet.measured_tenants, 2);
assert_eq!(
fleet.unmeasured_tenants, 2,
"the two fresh tenants are a hole, not two zeroes"
);
assert!(
(fleet.per_week / GIB as f64 - 2.0).abs() < 0.02,
"two tenants at 1 GiB/wk is a fleet at 2 GiB/wk, got {}",
fleet.per_week / GIB as f64
);
// A fleet where NOBODY can be measured is unknown, not a fleet at rest.
let fresh = state();
for t in ["c", "d"] {
fresh
.usage
.put(
"gunnar",
t,
&Usage {
meters: [("pack_bytes".to_string(), GIB)].into(),
measured_at_unix_ms: NOW,
},
)
.unwrap();
}
let none = ask(&fresh, "", "pack_bytes").await;
assert!(!none.enough_history, "no measurable tenant is UNKNOWN");
assert_eq!(none.measured_tenants, 0);
assert_eq!(none.unmeasured_tenants, 2);
}
async fn fleet(s: &Arc<AppState>) -> pb::FleetResponse {
UsageService(s.clone())
.fleet(Request::new(pb::FleetRequest {
product: "gunnar".into(),
}))
.await
.expect("gunnar is linked")
.into_inner()
}
/// **The wire carries the currency the provider priced in.** A sandbox
/// with a declared table in SEK — not the mock's EUR, so a constant would
/// be caught — and the fleet's cloud block says `SEK`. `2.30` alone is a
/// number an operator can misread; `2.30 SEK` is not.
#[cfg(feature = "sandbox")]
#[tokio::test]
async fn the_cloud_block_carries_the_currency_the_sandbox_priced_in() {
use monetize::testing::{purchase, MockPay};
use monetize_cloud::sandbox::{DeclaredPrices, Sandbox, SandboxConfig};
use monetize_cloud::{Resource, ServerId};
let mut cfg = SandboxConfig::new();
cfg.declared = Some(DeclaredPrices {
disk_minor_per_gib_month: 23,
plan_minor_per_month: Default::default(),
currency: monetize_pay::Currency::Sek,
});
let s = Arc::new(AppState::in_memory(
Registry::new()
.product(MockProduct::gunnar())
.cloud(Sandbox::new(cfg).unwrap()),
));
let mut p = purchase("alice");
p.resources = vec![Resource::Disk {
server: ServerId("gunnar-se-sto1".into()),
gib: 10,
}];
s.transaction(
s.registry.find_product("gunnar").unwrap(),
monetize_pay::Vendor::of(&MockPay::new()),
)
.run(&p)
.unwrap();
let c = fleet(&s).await.cloud.expect("always present");
assert!(c.available, "{}", c.error);
assert_eq!(
(c.provider.as_str(), c.owned.len(), c.monthly_minor),
("sandbox", 1, 230),
"{c:?}"
);
assert_eq!(
c.currency, "SEK",
"the declared table's currency — not the mock's EUR, not a default of the server's"
);
}
/// **CLOUD on the fleet: owned, then an orphan, then a missing.** The
/// provider's `live` map is drifted by hand — inserting makes an orphan,
/// removing makes a missing — and each drift shows up in exactly one list.
/// With no provider linked the block says so instead of claiming emptiness.
#[tokio::test]
async fn the_fleet_carries_the_cloud_inventory_reconciled_against_the_ledger() {
use monetize::testing::{purchase, Answer, MockCloud, MockPay};
use monetize_cloud::{Resource, ServerId};
let mock = Arc::new(MockCloud::new());
mock.answer_all(Answer::Provisioned);
let s = Arc::new(AppState::in_memory(
Registry::new()
.product(MockProduct::gunnar())
.cloud(mock.clone()),
));
let pay = MockPay::new();
let mut p = purchase("alice");
p.resources = vec![Resource::Disk {
server: ServerId("gunnar-se-sto1".into()),
gib: 10,
}];
let reference = s
.transaction(
s.registry.find_product("gunnar").unwrap(),
monetize_pay::Vendor::of(&pay),
)
.run(&p)
.unwrap()
.reference()
.to_owned();
let c = fleet(&s).await.cloud.expect("always present");
assert!(c.available, "{}", c.error);
assert_eq!(c.provider, "mockcloud");
assert_eq!(c.owned.len(), 1, "{c:?}");
let o = &c.owned[0];
assert_eq!(
(
o.id.as_str(),
o.kind.as_str(),
o.gib,
o.tenant.as_str(),
o.provenance.as_str()
),
("res-0", "disk", 10, "alice", "mockcloud")
);
assert_eq!(
o.reference, reference,
"the ledger's reference, not the label's (a mock has none)"
);
assert_eq!(o.attached_to, "gunnar-se-sto1");
assert!(o.priced && o.monthly_minor == 1_000);
assert_eq!(
(c.total_bytes, c.monthly_minor, c.unpriced, c.foreign_count),
(10 << 30, 1_000, 0, 0)
);
assert_eq!(
c.currency, "EUR",
"the mock prices in EUR and the wire says so"
);
assert_eq!(
c.account, "mockcloud",
"the linked cloud's slot names the account (MAP-3)"
);
assert!(c.orphans.is_empty() && c.missing.is_empty(), "{c:?}");
// RED: the provider has a disk the ledger never sold.
mock.live.lock().unwrap().insert(
"res-forgotten".into(),
Resource::Disk {
server: ServerId("gunnar-se-sto1".into()),
gib: 5,
},
);
let c = fleet(&s).await.cloud.unwrap();
assert_eq!(
c.orphans.iter().map(|o| o.id.as_str()).collect::<Vec<_>>(),
vec!["res-forgotten"]
);
assert_eq!(c.owned.len(), 1, "the orphan is not matched to anything");
assert_eq!(
c.total_bytes,
10 << 30,
"an orphan is not this product's capacity"
);
// RED: the ledger says alice holds res-0 and the provider no longer has it.
mock.live.lock().unwrap().remove("res-0");
let c = fleet(&s).await.cloud.unwrap();
assert!(c.owned.is_empty(), "{c:?}");
let m = &c.missing[0];
assert_eq!(
(m.id.as_str(), m.tenant.as_str(), m.resource.as_str()),
("res-0", "alice", "disk:gunnar-se-sto1:10")
);
assert_eq!(m.reference, reference);
assert_eq!(c.missing.len(), 1);
assert_eq!(c.orphans.len(), 1, "the orphan is still there");
// No provider linked: not an empty inventory, an unavailable one.
let c = fleet(&state()).await.cloud.unwrap();
assert!(!c.available);
assert!(c.error.contains("no cloud provider"), "{}", c.error);
assert_eq!(c.provider, "none");
}
fn meter(f: &pb::FleetResponse, name: &str) -> pb::FleetMeter {
f.meters
.iter()
.find(|m| m.meter == name)
.unwrap_or_else(|| {
panic!(
"{name} must have a fleet total: {:?}",
f.meters.iter().map(|m| &m.meter).collect::<Vec<_>>()
)
})
.clone()
}
/// (r) **The oversell number on the wire, and the per-product meter that is
/// NOT on it.**
///
/// Ten tenants sold 10 GiB each on a box with 55 GiB servable. And
/// `cpu_millicores` — which every one of them has a cap on — appears in no
/// entry at all, because nobody measured a tenant's cores.
#[tokio::test]
async fn the_fleet_carries_the_oversell_and_leaves_per_product_meters_out() {
let s = state();
for i in 0..10 {
let tenant = format!("t{i}");
store(&s, &tenant, 1, |_| GIB);
let fact = monetize_product::EntitlementFact {
tenant: monetize_product::TenantId(tenant.clone()),
plan: "10gb-se".into(),
state: monetize_product::State::Paid,
paid_until_unix_ms: Some(NOW + DAY),
caps: [
("pack_bytes".to_string(), 10 * GIB),
("cpu_millicores".to_string(), 2000),
]
.into(),
source: "operator".into(),
signature: vec![],
issued_unix_ms: None,
issued_signature: vec![],
};
s.ledger
.set_entitlement("gunnar", &s.signer_for("gunnar").sign_fact(fact))
.unwrap();
}
s.health.set_servable(
"gunnar",
measured(
55 * GIB,
75 * GIB,
monetize_product::Growth::Unknown("not asked".into()),
),
NOW,
);
let f = fleet(&s).await;
assert!(f.servable_measured);
assert_eq!(
(f.servable_bytes, f.total_bytes, f.servable_at_unix_ms),
(55 * GIB, 75 * GIB, NOW)
);
let pack = meter(&f, "pack_bytes");
assert_eq!(
(pack.tenants, pack.used, pack.allowance),
(10, 10 * GIB, 100 * GIB)
);
assert_eq!(pack.oversell, "oversold");
assert_eq!(
pack.oversell_bytes,
35 * GIB,
"100 GiB promised against 10 held plus 55 servable"
);
assert_eq!(pack.capacity, Some(65 * GIB));
assert!(!pack.is_a_floor);
// THE PER-PRODUCT METER IS ABSENT, by name. Every tenant carries a
// `cpu_millicores` cap, so a fold that ignored scope would happily
// report 20 000 millicores of fleet CPU — a number nobody measured.
assert!(
!f.meters
.iter()
.any(|m| m.meter == "cpu_millicores" || m.meter == "ram_bytes"),
"a per-product meter must not appear in a per-tenant sum: {:?}",
f.meters.iter().map(|m| &m.meter).collect::<Vec<_>>()
);
assert!(
f.meters.iter().any(|m| m.meter == "pack_bytes"),
"the assertion above must not be vacuous"
);
}
/// (r) **A product the poller has not asked yet reports UNMEASURED, not a
/// fleet that fits comfortably in zero bytes.** The state a freshly started
/// server is in, and the state the live gunnar.rs appliance is in
/// permanently until it is upgraded past the `Admin.Capacity` RPC.
#[tokio::test]
async fn an_unasked_product_reports_unmeasured_and_not_a_verdict() {
let s = state();
store(&s, "a", 1, |_| 4 * GIB);
let fact = monetize_product::EntitlementFact {
tenant: monetize_product::TenantId("a".into()),
plan: "10gb-se".into(),
state: monetize_product::State::Paid,
paid_until_unix_ms: Some(NOW + DAY),
caps: [("pack_bytes".to_string(), 10 * GIB)].into(),
source: "operator".into(),
signature: vec![],
issued_unix_ms: None,
issued_signature: vec![],
};
s.ledger
.set_entitlement("gunnar", &s.signer_for("gunnar").sign_fact(fact))
.unwrap();
// Nothing has been recorded: the poller has not run.
let f = fleet(&s).await;
assert!(!f.servable_measured);
assert_eq!(f.servable_at_unix_ms, 0, "never asked");
assert!(
f.servable_reason.contains("has not yet asked"),
"{}",
f.servable_reason
);
let pack = meter(&f, "pack_bytes");
assert_eq!(
(pack.used, pack.allowance),
(4 * GIB, 10 * GIB),
"the halves monetize CAN fold are still folded"
);
assert_eq!(pack.oversell, "unmeasured", "silence is not a verdict");
assert_eq!(pack.oversell_bytes, 0);
assert_eq!(pack.capacity, None, "an unmeasured box has no denominator");
assert!(pack.is_a_floor);
// A gunnar that answered "I could not measure" is the same arm with a
// different reason — and still not a number.
s.health.set_servable(
"gunnar",
monetize_product::Servable::Unmeasured("no Admin.Capacity RPC".into()),
NOW,
);
let f = fleet(&s).await;
assert!(!f.servable_measured);
assert_eq!(f.servable_at_unix_ms, NOW, "asked, and it could not say");
assert_eq!(
meter(&f, "pack_bytes").oversell_reason,
"no Admin.Capacity RPC"
);
// And the fits-fine arm on the SAME numbers, so neither is unreachable.
s.health.set_servable(
"gunnar",
measured(
100 * GIB,
128 * GIB,
monetize_product::Growth::Unknown("not asked".into()),
),
NOW,
);
let pack = meter(&fleet(&s).await, "pack_bytes");
assert_eq!(pack.oversell, "fits");
assert_eq!(pack.oversell_bytes, 94 * GIB);
assert!(!pack.is_a_floor);
}
fn measured(
servable_bytes: u64,
total_bytes: u64,
growth: monetize_product::Growth,
) -> monetize_product::Servable {
monetize_product::Servable::Measured {
servable_bytes,
total_bytes,
growth,
ceilings: Default::default(),
}
}
/// (r) **A SEALED box says so on the wire, with the product's own sentence;
/// an unmeasured box says nothing about growth at all.**
///
/// RED before `growth` was on `FleetResponse`: the console's `⚠ cannot
/// grow` had no field to read, and a fleet that was full AND sealed looked
/// exactly like one that wanted a disk.
#[tokio::test]
async fn a_sealed_fleet_carries_the_growth_fact_and_an_unmeasured_one_carries_none() {
let s = state();
store(&s, "a", 1, |_| 4 * GIB);
// Never asked: no growth claim of any kind.
let f = fleet(&s).await;
assert!(!f.servable_measured);
assert_eq!(
(f.growth.as_str(), f.growth_detail.as_str()),
("", ""),
"an unmeasured box has not said whether it can grow"
);
let sentence = "a disk bought and attached to this box would be BILLED AND INVISIBLE. Serving more needs a fresh install";
s.health.set_servable(
"gunnar",
measured(
55 * GIB,
75 * GIB,
monetize_product::Growth::Sealed(sentence.into()),
),
NOW,
);
let f = fleet(&s).await;
assert_eq!(f.growth, "sealed");
assert_eq!(
f.growth_detail, sentence,
"the product's sentence, verbatim"
);
// UNKNOWN is its own word, never folded into sealed.
s.health.set_servable(
"gunnar",
measured(
55 * GIB,
75 * GIB,
monetize_product::Growth::Unknown("could not tell".into()),
),
NOW,
);
let f = fleet(&s).await;
assert_eq!(f.growth, "unknown");
assert_eq!(f.growth_detail, "could not tell");
s.health.set_servable(
"gunnar",
measured(
55 * GIB,
75 * GIB,
monetize_product::Growth::OperatorOnly("extend it by hand".into()),
),
NOW,
);
assert_eq!(fleet(&s).await.growth, "operator_only");
}
/// (r) A rate with no meter named is refused, not defaulted. A default meter
/// would quote a number against a meter the caller never asked about.
#[tokio::test]
async fn refuse_twin_a_rate_needs_a_meter_and_a_known_product() {
let s = state();
let err = UsageService(s.clone())
.trend(Request::new(pb::TrendRequest {
product: "gunnar".into(),
tenant: "a".into(),
meter: String::new(),
window_ms: 0,
}))
.await
.expect_err("no meter must refuse");
assert_eq!(err.code(), tonic::Code::InvalidArgument);
assert!(err.message().contains("meter"), "{}", err.message());
let err = UsageService(s)
.trend(Request::new(pb::TrendRequest {
product: "idun".into(),
tenant: String::new(),
meter: "pack_bytes".into(),
window_ms: 0,
}))
.await
.expect_err("an unlinked product must refuse");
assert_eq!(err.code(), tonic::Code::NotFound);
}
// ── the estate orders' laid iron on the CLOUD panel (2026-10-22) ──
/// A provider whose listing is fixed by the test.
struct Listed(Vec<monetize_cloud::Owned>);
impl Listed {
pub fn provider(&self) -> &'static str {
"upcloud"
}
pub fn zones(&self) -> &[monetize_cloud::Zone] {
&[]
}
pub fn price_resource(
&self,
_: &monetize_cloud::Resource,
) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
Err(monetize_cloud::CloudError::Refused(
"not in this test".into(),
))
}
pub fn ensure(
&self,
_: &str,
_: &monetize_cloud::Resource,
) -> Result<monetize_cloud::Ensured, monetize_cloud::CloudError> {
Err(monetize_cloud::CloudError::Refused(
"nothing is bought in this test".into(),
))
}
pub fn destroy_id(
&self,
_: &monetize_cloud::ResourceId,
) -> Result<(), monetize_cloud::CloudError> {
panic!("the panel never cleans anything up")
}
pub fn status(
&self,
_: &monetize_cloud::ResourceId,
) -> Result<monetize_cloud::ResourceStatus, monetize_cloud::CloudError> {
Err(monetize_cloud::CloudError::Refused(
"not in this test".into(),
))
}
pub fn list_owned(&self) -> Result<monetize_cloud::Inventory, monetize_cloud::CloudError> {
Ok(monetize_cloud::Inventory {
owned: self.0.clone(),
foreign: 0,
currency: monetize_pay::Currency::Eur,
})
}
}
impl monetize_cloud::cloud::Cloud for Listed {
fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
monetize_cloud::cloud::CloudManifest {
provider: self.provider().to_string(),
zones: self.zones().to_vec(),
settings: Vec::new(),
grow_refused: Some(monetize_cloud::grow::unsupported(self.provider()).to_string()),
}
}
fn catalog(
&self,
_zone: &monetize_cloud::Zone,
) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn price(
&self,
what: &monetize_cloud::cloud::Priced<'_>,
) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
monetize_cloud::cloud::Priced::Plan(p) => {
monetize_cloud::price_estate_by_rows(p, |r| self.price_resource(r), None)
}
monetize_cloud::cloud::Priced::Grow(_set) => {
Err(monetize_cloud::grow::unsupported(self.provider()))
}
}
}
fn lay(
&self,
_plan: &monetize_cloud::lay::EstatePlan,
_journal: &monetize_cloud::grow::Journal,
) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn change(
&self,
change: &monetize_cloud::cloud::Change,
_journal: &monetize_cloud::grow::Journal,
) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
Ok(match change {
monetize_cloud::cloud::Change::Plan {
server: _,
plan: _,
delta: _,
} => {
return Err(monetize_cloud::lay::unsupported_verb(
self.provider(),
"change_plan",
))
}
monetize_cloud::cloud::Change::Grow { set: _, estate: _ } => {
return Err(monetize_cloud::grow::unsupported(self.provider()))
}
monetize_cloud::cloud::Change::Settle { set: _ } => {
return Err(monetize_cloud::grow::unsupported(self.provider()))
}
monetize_cloud::cloud::Change::ForgetMedia { laid: _ } => {
monetize_cloud::cloud::Changed::Forgotten(Vec::new())
}
monetize_cloud::cloud::Change::Ensure {
reference,
resource,
room: _,
} => monetize_cloud::cloud::Changed::Ensured(self.ensure(reference, resource)?),
monetize_cloud::cloud::Change::Retire { reference, machine } => {
monetize_cloud::cloud::Changed::Retired(monetize_cloud::cloud::retire_by_rows(
self, reference, machine,
)?)
}
monetize_cloud::cloud::Change::LabelAdopted { .. } => {
return Err(monetize_cloud::lay::unsupported_verb(
self.provider(),
"label_adopted",
))
}
})
}
fn boot(
&self,
boot: &monetize_cloud::cloud::Boot,
_journal: &monetize_cloud::grow::Journal,
) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
match boot {
monetize_cloud::cloud::Boot::Keep(_k) => Err(
monetize_cloud::lay::unsupported_verb(self.provider(), "keep_boot"),
),
monetize_cloud::cloud::Boot::Power { server: _, on: _ } => Err(
monetize_cloud::lay::unsupported_verb(self.provider(), "set_power"),
),
monetize_cloud::cloud::Boot::Restore(_r) => Err(
monetize_cloud::lay::unsupported_verb(self.provider(), "restore_from_backup"),
),
}
}
fn firewall(
&self,
_laid: &monetize_cloud::lay::LaidEstate,
) -> Result<(), monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported_verb(
self.provider(),
"set_estate_firewall",
))
}
fn destroy(
&self,
what: &monetize_cloud::cloud::Give<'_>,
) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Give::References(refs) => Ok(
monetize_cloud::release_for_by_rows(self.list_owned()?, refs, |id| {
self.destroy_id(id)
}),
),
monetize_cloud::cloud::Give::Resource(id) => {
self.destroy_id(id)?;
Ok(monetize_cloud::Reclaimed {
destroyed: vec![(*id).clone()],
..monetize_cloud::Reclaimed::default()
})
}
}
}
fn inventory(
&self,
what: &monetize_cloud::cloud::Listing<'_>,
) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
Ok(match what {
monetize_cloud::cloud::Listing::Account => {
monetize_cloud::cloud::Listed::Account(self.list_owned()?)
}
monetize_cloud::cloud::Listing::Set(_set) => {
return Err(monetize_cloud::grow::unsupported(self.provider()))
}
monetize_cloud::cloud::Listing::Status(id) => {
monetize_cloud::cloud::Listed::Status(self.status(id)?)
}
monetize_cloud::cloud::Listing::Under(refs) => {
monetize_cloud::cloud::Listed::Under(monetize_cloud::cloud::under(
self.list_owned()?,
refs,
))
}
monetize_cloud::cloud::Listing::Room { delta: _, room: _ } => {
monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft {
serve: Ok(()),
left_bytes: None,
ceilings: Default::default(),
})
}
monetize_cloud::cloud::Listing::Admits(_plan) => {
monetize_cloud::cloud::Listed::Admitted
}
})
}
}
fn listed(id: &str, kind: monetize_cloud::OwnedKind, reference: &str) -> monetize_cloud::Owned {
monetize_cloud::Owned {
id: monetize_cloud::ResourceId {
provider: "upcloud".into(),
id: id.into(),
},
kind,
gib: 20,
zone: "se-sto1".into(),
attached_to: None,
reference: Some(reference.into()),
monthly_minor: Some(100),
currency: monetize_pay::Currency::Eur,
provenance: "upcloud".into(),
resource: None,
backup: None,
}
}
/// acme's estate as the shop laid it: front, main, twin, each a server and two disks.
fn acme_laid() -> LaidEstate {
use monetize_cloud::lay::{LaidDisk, LaidMachine};
LaidEstate {
reference: "estate/acme".into(),
company: "acme".into(),
zone: monetize_cloud::Zone("se-sto1".into()),
machines: ["front", "main", "twin"]
.iter()
.map(|role| LaidMachine {
role: (*role).into(),
hostname: format!("{role}.acme"),
title: format!("acme-{role}"),
plan: "2xCPU-4GB".into(),
server: format!("srv-{role}"),
public_ipv4: String::new(),
utility_ipv4: String::new(),
disks: (0..2)
.map(|i| LaidDisk {
title: format!("acme-{role}-disk{i}"),
id: format!("disk-{role}-{i}"),
gib: 20,
tier: "maxiops".into(),
address: format!("virtio:{i}"),
})
.collect(),
medium_clone: String::new(),
})
.collect(),
narrate_from: String::new(),
shape: None,
}
}
/// **RED (2026-10-22): a shop-laid estate is OWNED, not orphaned.** The ONE box's
/// CLOUD panel reconciled the provider against the ledger alone, and an estate
/// order keeps its iron in its `laid` record — so all nine of acme's resources
/// read "we pay for this and no order owns it", and the estate's own panel said
/// "nothing owned". Laid with bare ids, listed with kind prefixes (`server/…`,
/// `storage/…`) as UpCloud lists them. One laid disk is gone at the provider:
/// MISSING. One labelled resource no run laid: still an ORPHAN.
#[test]
fn an_estate_the_shop_laid_is_owned_under_its_estate_and_never_an_orphan() {
use monetize_cloud::OwnedKind;
let s = state();
let mut at_provider = Vec::new();
for role in ["front", "main", "twin"] {
at_provider.push(listed(
&format!("server/srv-{role}"),
OwnedKind::Server,
"estate/acme",
));
for i in 0..2 {
if (role, i) == ("twin", 1) {
continue; // gone at the provider
}
at_provider.push(listed(
&format!("storage/disk-{role}-{i}"),
OwnedKind::Disk,
"estate/acme",
));
}
}
at_provider.push(listed("storage/stray", OwnedKind::Disk, "estate/ghost"));
let cloud = Listed(at_provider);
let estates = EstateIron {
product: Some("gunnar".into()),
laid: vec![LaidIron::from_parts(
phase::CAPTURED,
"estate/acme",
Some(&acme_laid()),
&[],
)
.unwrap()],
};
let c = cloud_inventory_pb("gunnar", &cloud, &s.ledger, &estates);
assert!(c.available, "{}", c.error);
assert_eq!(
c.orphans.iter().map(|o| o.id.as_str()).collect::<Vec<_>>(),
vec!["storage/stray"],
"only the unlaid one is an orphan: {c:?}"
);
assert_eq!(c.owned.len(), 8, "3 servers + 5 standing disks: {c:?}");
assert!(
c.owned
.iter()
.all(|o| o.reference == "estate/acme" && o.tenant == "acme"),
"{c:?}"
);
assert_eq!(c.monthly_minor, 800);
assert_eq!(
c.missing
.iter()
.map(|m| (m.id.as_str(), m.reference.as_str()))
.collect::<Vec<_>>(),
vec![("disk-twin-1", "estate/acme")],
"{c:?}"
);
// Another product's panel does not list acme as its own, nor as orphans.
let other = cloud_inventory_pb("holger", &cloud, &s.ledger, &estates);
assert!(
other.owned.is_empty() && other.missing.is_empty(),
"{other:?}"
);
assert_eq!(other.orphans.len(), 1, "{other:?}");
// Twin: without the laid record, every one of them is an orphan again.
let bare = cloud_inventory_pb("gunnar", &cloud, &s.ledger, &EstateIron::default());
assert_eq!(bare.orphans.len(), 9, "{bare:?}");
}
/// A gone run owns nothing, a run not yet laid owns nothing, and an id the
/// give-back already asked to go is not "missing".
#[test]
fn a_gone_or_unlaid_run_owns_nothing_and_a_given_back_id_is_not_missing() {
let laid = acme_laid();
assert_eq!(
LaidIron::from_parts(phase::TERMINATED, "estate/acme", Some(&laid), &[]),
None
);
assert_eq!(
LaidIron::from_parts(phase::RELEASED, "estate/acme", Some(&laid), &[]),
None
);
assert_eq!(
LaidIron::from_parts(phase::LAYING, "estate/acme", None, &[]),
None
);
let terminating = LaidIron::from_parts(
phase::TERMINATING,
"estate/acme",
Some(&laid),
&["server/srv-front".into()],
)
.unwrap();
let rows = estate_holdings("upcloud", &[], &[terminating]);
assert_eq!(rows.len(), 8, "front's server was given back; its disks and the other boxes are still missing: {rows:?}");
assert!(
rows.iter().all(|h| h.reference == "estate/acme"
&& h.id.as_ref().is_some_and(|i| i.provider == "upcloud")),
"{rows:?}"
);
assert!(
!rows
.iter()
.any(|h| h.id.as_ref().unwrap().id == "srv-front"),
"{rows:?}"
);
}
/// One site's laid record: front, main and twin, one disk each, under `estate/<company>`.
fn site_laid(company: &str) -> LaidEstate {
let mut l = acme_laid();
l.reference = format!("estate/{company}");
l.company = company.into();
for m in &mut l.machines {
m.server = format!("{company}-{}", m.role);
m.disks.truncate(1);
m.disks[0].id = format!("{company}-{}-disk", m.role);
}
l
}
/// **RED MAP-SEG 1 (owner 2026-10-26, the Map of test2-gunnar-rs):** the Map of ONE estate
/// listed the cloud iron of EVERY estate on the account (rickardtest, acme2, ignalina,
/// test2holgerrs, holgerrs, test2gunnarrs), because no row said whose it was. Every row
/// of the inventory now names its estate — a site's by its LABEL (the name its
/// `/e/<estate>/` pages carry), a sold estate's by its company, a tenant's holding of a
/// sold estate by that estate — and a row of no ONE estate names none.
#[test]
fn every_inventory_row_names_the_estate_it_belongs_to() {
use monetize_cloud::OwnedKind;
let s = state();
let mut at_provider = Vec::new();
for (company, roles) in [
("test2gunnarrs", ["front", "main", "twin"]),
("acme", ["front", "main", "twin"]),
] {
for role in roles {
at_provider.push(listed(
&format!("server/{company}-{role}"),
OwnedKind::Server,
&format!("estate/{company}"),
));
at_provider.push(listed(
&format!("storage/{company}-{role}-disk"),
OwnedKind::Disk,
&format!("estate/{company}"),
));
}
}
at_provider.push(listed(
"storage/left-behind",
OwnedKind::Disk,
"estate/holgerr34e",
));
let cloud = Listed(at_provider);
let estates = EstateIron {
product: Some("gunnar".into()),
laid: vec![
LaidIron::from_parts(
phase::CAPTURED,
"estate/test2gunnarrs",
Some(&site_laid("test2gunnarrs")),
&[],
)
.unwrap()
.named("test2-gunnar-rs"),
LaidIron::from_parts(
phase::CAPTURED,
"estate/acme",
Some(&site_laid("acme")),
&[],
)
.unwrap()
.named("acme"),
],
};
let c = cloud_inventory_pb("gunnar", &cloud, &s.ledger, &estates);
assert_eq!(c.owned.len(), 12, "{c:?}");
for o in &c.owned {
let want = if o.reference == "estate/acme" {
"acme"
} else {
"test2-gunnar-rs"
};
assert_eq!(o.estate, want, "a row says whose it is: {o:?}");
}
assert_eq!(
c.orphans
.iter()
.map(|o| (o.id.as_str(), o.estate.as_str()))
.collect::<Vec<_>>(),
vec![("storage/left-behind", "holgerr34e")],
"an orphan's label still names the estate it was laid for: {c:?}"
);
// The rule itself, every arm.
assert_eq!(
estate_of("estate/test2gunnarrs", &estates.laid),
"test2-gunnar-rs"
);
assert_eq!(estate_of("estate/gone2", &estates.laid), "gone2");
assert_eq!(
estate_of("gunnar/@acme/team/2026-10-01", &estates.laid),
"acme"
);
assert_eq!(
estate_of("gunnar/alice/2026-10-01", &estates.laid),
"",
"a product's own tenant is no ONE estate's"
);
assert_eq!(estate_of("", &estates.laid), "");
}
/// A cloud whose labelled listing is `listed` and where the unlabelled `standing` ids stand
/// (a kit's machines): `status` answers by uuid, as UpCloud's `GET /server/<uuid>` does.
struct Standing {
listed: Vec<monetize_cloud::Owned>,
standing: Vec<String>,
}
impl Standing {
pub fn provider(&self) -> &'static str {
"upcloud"
}
pub fn zones(&self) -> &[monetize_cloud::Zone] {
&[]
}
pub fn price_resource(
&self,
_: &monetize_cloud::Resource,
) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
Err(monetize_cloud::CloudError::Refused(
"not in this test".into(),
))
}
pub fn ensure(
&self,
_: &str,
_: &monetize_cloud::Resource,
) -> Result<monetize_cloud::Ensured, monetize_cloud::CloudError> {
Err(monetize_cloud::CloudError::Refused(
"nothing is bought in this test".into(),
))
}
pub fn destroy_id(
&self,
_: &monetize_cloud::ResourceId,
) -> Result<(), monetize_cloud::CloudError> {
panic!("the panel never cleans anything up")
}
pub fn status(
&self,
id: &monetize_cloud::ResourceId,
) -> Result<monetize_cloud::ResourceStatus, monetize_cloud::CloudError> {
let bare = id.id.rsplit('/').next().unwrap_or_default();
Ok(if self.standing.iter().any(|s| s == bare) {
monetize_cloud::ResourceStatus::Active
} else {
monetize_cloud::ResourceStatus::Gone
})
}
pub fn list_owned(&self) -> Result<monetize_cloud::Inventory, monetize_cloud::CloudError> {
Ok(monetize_cloud::Inventory {
owned: self.listed.clone(),
foreign: 6,
currency: monetize_pay::Currency::Eur,
})
}
}
impl monetize_cloud::cloud::Cloud for Standing {
fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
monetize_cloud::cloud::CloudManifest {
provider: self.provider().to_string(),
zones: self.zones().to_vec(),
settings: Vec::new(),
grow_refused: Some(monetize_cloud::grow::unsupported(self.provider()).to_string()),
}
}
fn catalog(
&self,
_zone: &monetize_cloud::Zone,
) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn price(
&self,
what: &monetize_cloud::cloud::Priced<'_>,
) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
monetize_cloud::cloud::Priced::Plan(p) => {
monetize_cloud::price_estate_by_rows(p, |r| self.price_resource(r), None)
}
monetize_cloud::cloud::Priced::Grow(_set) => {
Err(monetize_cloud::grow::unsupported(self.provider()))
}
}
}
fn lay(
&self,
_plan: &monetize_cloud::lay::EstatePlan,
_journal: &monetize_cloud::grow::Journal,
) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn change(
&self,
change: &monetize_cloud::cloud::Change,
_journal: &monetize_cloud::grow::Journal,
) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
Ok(match change {
monetize_cloud::cloud::Change::Plan {
server: _,
plan: _,
delta: _,
} => {
return Err(monetize_cloud::lay::unsupported_verb(
self.provider(),
"change_plan",
))
}
monetize_cloud::cloud::Change::Grow { set: _, estate: _ } => {
return Err(monetize_cloud::grow::unsupported(self.provider()))
}
monetize_cloud::cloud::Change::Settle { set: _ } => {
return Err(monetize_cloud::grow::unsupported(self.provider()))
}
monetize_cloud::cloud::Change::ForgetMedia { laid: _ } => {
monetize_cloud::cloud::Changed::Forgotten(Vec::new())
}
monetize_cloud::cloud::Change::Ensure {
reference,
resource,
room: _,
} => monetize_cloud::cloud::Changed::Ensured(self.ensure(reference, resource)?),
monetize_cloud::cloud::Change::Retire { reference, machine } => {
monetize_cloud::cloud::Changed::Retired(monetize_cloud::cloud::retire_by_rows(
self, reference, machine,
)?)
}
monetize_cloud::cloud::Change::LabelAdopted { .. } => {
return Err(monetize_cloud::lay::unsupported_verb(
self.provider(),
"label_adopted",
))
}
})
}
fn boot(
&self,
boot: &monetize_cloud::cloud::Boot,
_journal: &monetize_cloud::grow::Journal,
) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
match boot {
monetize_cloud::cloud::Boot::Keep(_k) => Err(
monetize_cloud::lay::unsupported_verb(self.provider(), "keep_boot"),
),
monetize_cloud::cloud::Boot::Power { server: _, on: _ } => Err(
monetize_cloud::lay::unsupported_verb(self.provider(), "set_power"),
),
monetize_cloud::cloud::Boot::Restore(_r) => Err(
monetize_cloud::lay::unsupported_verb(self.provider(), "restore_from_backup"),
),
}
}
fn firewall(
&self,
_laid: &monetize_cloud::lay::LaidEstate,
) -> Result<(), monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported_verb(
self.provider(),
"set_estate_firewall",
))
}
fn destroy(
&self,
what: &monetize_cloud::cloud::Give<'_>,
) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Give::References(refs) => Ok(
monetize_cloud::release_for_by_rows(self.list_owned()?, refs, |id| {
self.destroy_id(id)
}),
),
monetize_cloud::cloud::Give::Resource(id) => {
self.destroy_id(id)?;
Ok(monetize_cloud::Reclaimed {
destroyed: vec![(*id).clone()],
..monetize_cloud::Reclaimed::default()
})
}
}
}
fn inventory(
&self,
what: &monetize_cloud::cloud::Listing<'_>,
) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
Ok(match what {
monetize_cloud::cloud::Listing::Account => {
monetize_cloud::cloud::Listed::Account(self.list_owned()?)
}
monetize_cloud::cloud::Listing::Set(_set) => {
return Err(monetize_cloud::grow::unsupported(self.provider()))
}
monetize_cloud::cloud::Listing::Status(id) => {
monetize_cloud::cloud::Listed::Status(self.status(id)?)
}
monetize_cloud::cloud::Listing::Under(refs) => {
monetize_cloud::cloud::Listed::Under(monetize_cloud::cloud::under(
self.list_owned()?,
refs,
))
}
monetize_cloud::cloud::Listing::Room { delta: _, room: _ } => {
monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft {
serve: Ok(()),
left_bytes: None,
ceilings: Default::default(),
})
}
monetize_cloud::cloud::Listing::Admits(_plan) => {
monetize_cloud::cloud::Listed::Admitted
}
})
}
}
/// **RED MAP-SEG 4 (owner 2026-10-26: "3 MISSING gunnarrs servers"):** gunnar.rs was
/// installed by its kit and ADOPTED by the ONE box. Its machines stand on this box's account
/// (the box keep-booted its front through it) but carry no label of this box, so the
/// labelled listing never holds them, and the panel called all three "sold, but the cloud
/// does not have it". The inventory now looks an adopted estate's machines up BY UUID: the
/// three that stand are owned under the estate, not missing — and a machine that is really
/// gone still IS missing.
#[test]
fn an_adopted_estates_standing_machines_are_owned_and_a_gone_one_is_missing() {
let s = state();
let adopted = LaidIron::from_parts(
phase::CAPTURED,
"estate/gunnarrs",
Some(&site_laid("gunnarrs")),
&[],
)
.unwrap()
.named("gunnar-rs")
.adopted_if(true);
let estates = EstateIron {
product: Some("gunnar".into()),
laid: vec![adopted],
};
let all: Vec<String> = ["front", "main", "twin"]
.iter()
.flat_map(|r| [format!("gunnarrs-{r}"), format!("gunnarrs-{r}-disk")])
.collect();
let c = cloud_inventory_pb(
"gunnar",
&Standing {
listed: Vec::new(),
standing: all.clone(),
},
&s.ledger,
&estates,
);
assert!(
c.missing.is_empty(),
"an adopted estate whose machines stand is not missing: {:?}",
c.missing
);
assert_eq!(
c.owned.len(),
6,
"its 3 servers and 3 disks are owned under it: {c:?}"
);
assert!(
c.owned
.iter()
.all(|o| o.estate == "gunnar-rs" && o.reference == "estate/gunnarrs" && !o.priced),
"{c:?}"
);
assert!(c.orphans.is_empty(), "{c:?}");
// The main is really gone: it, and its disk, ARE missing.
let left: Vec<String> = all
.iter()
.filter(|i| !i.starts_with("gunnarrs-main"))
.cloned()
.collect();
let c = cloud_inventory_pb(
"gunnar",
&Standing {
listed: Vec::new(),
standing: left,
},
&s.ledger,
&estates,
);
let mut gone: Vec<&str> = c.missing.iter().map(|m| m.id.as_str()).collect();
gone.sort();
assert_eq!(
gone,
vec!["gunnarrs-main", "gunnarrs-main-disk"],
"{:?}",
c.missing
);
assert!(c.missing.iter().all(|m| m.estate == "gunnar-rs"));
}
/// A provider-side BACKUP row as UpCloud lists one: a storage of type backup, with the
/// `origin` it was taken of, the labels of that disk copied onto it (`reference`), or
/// none (a kit's disk, the ONE box's own disk).
fn backup(
id: &str,
origin: &str,
standing: bool,
server: &str,
created: &str,
reference: Option<&str>,
title: &str,
) -> monetize_cloud::Owned {
let mut b = listed(
id,
monetize_cloud::OwnedKind::Disk,
reference.unwrap_or_default(),
);
b.reference = reference.map(str::to_owned);
b.gib = 10;
b.monthly_minor = None;
b.backup = Some(monetize_cloud::BackupOf {
origin: origin.into(),
origin_standing: standing,
server: server.into(),
created: created.into(),
title: title.into(),
});
b
}
/// **RED MAP-3 (owner 2026-10-27, the ONE box's Iron page):** 16 UpCloud BACKUPS of the
/// live estates holger-rs, test2-gunnar-rs and test2-holger-rs ("Simple backup of
/// gunnar-front-test2gunnarrs system", …) read "⚠ orphan — we pay for this and no order
/// owns it": a backup carries the labels of the disk it was taken of, and no order laid it.
/// A backup belongs to the estate of its ORIGIN disk: it is listed under that estate as
/// `backup of <server>, <date>`, counted apart from the owned disks, never as an orphan.
/// A backup whose disk is gone is an orphan, by its title. The daily backups of a kit
/// estate the box adopted (gunnar-rs, "Scheduled backup of gunnar-front") belong to it
/// the same way, and the ONE box's own disk's ("monetize-prod") are the account's own,
/// no estate's: a backup, not an orphan either.
#[test]
fn a_backup_belongs_to_its_disks_estate_and_is_an_orphan_only_when_its_disk_is_gone() {
use monetize_cloud::OwnedKind;
let s = state();
let mut listed_rows = Vec::new();
for role in ["front", "main", "twin"] {
listed_rows.push(listed(
&format!("server/test2gunnarrs-{role}"),
OwnedKind::Server,
"estate/test2gunnarrs",
));
let mut d = listed(
&format!("storage/test2gunnarrs-{role}-disk"),
OwnedKind::Disk,
"estate/test2gunnarrs",
);
d.attached_to = Some(format!("test2gunnarrs-{role}"));
listed_rows.push(d);
}
let front = "storage/test2gunnarrs-front-disk";
let site = |id: &str, created: &str| {
backup(
id,
front,
true,
"gunnar-front-test2gunnarrs",
created,
Some("estate/test2gunnarrs"),
"Simple backup of gunnar-front-test2gunnarrs system",
)
};
listed_rows.push(site("storage/bk-site-27", "2026-10-27T03:56:43Z"));
listed_rows.push(site("storage/bk-site-26", "2026-10-26T03:51:35Z"));
// Of a disk that is gone: an orphan, named.
listed_rows.push(backup(
"storage/bk-gone",
"storage/old-front-disk",
false,
"",
"2026-10-20T04:00:00Z",
Some("estate/test2gunnarrs"),
"Simple backup of gunnar-front-old system",
));
// Of the adopted gunnar-rs front (its disk carries no label, nor does its backup).
listed_rows.push(backup(
"storage/bk-adopted",
"storage/gunnarrs-front-disk",
true,
"gunnar-front",
"2026-10-27T03:58:58Z",
None,
"Scheduled backup of gunnar-front system",
));
// Of the ONE box's own disk: the account's own, no estate's.
listed_rows.push(backup(
"storage/bk-box",
"storage/monetize-prod-disk",
true,
"monetize-prod",
"2026-10-27T03:48:43Z",
None,
"Scheduled backup of monetize-prod system",
));
// Of a labelled disk no order laid (itself an orphan): an orphan too.
let mut stray = listed("storage/stray", OwnedKind::Disk, "estate/ghost");
stray.gib = 40;
listed_rows.push(stray);
listed_rows.push(backup(
"storage/bk-stray",
"storage/stray",
true,
"",
"2026-10-27T01:00:00Z",
Some("estate/ghost"),
"Simple backup of ghost system",
));
let adopted = LaidIron::from_parts(
phase::CAPTURED,
"estate/gunnarrs",
Some(&site_laid("gunnarrs")),
&[],
)
.unwrap()
.named("gunnar-rs")
.adopted_if(true);
let site_run = LaidIron::from_parts(
phase::CAPTURED,
"estate/test2gunnarrs",
Some(&site_laid("test2gunnarrs")),
&[],
)
.unwrap()
.named("test2-gunnar-rs");
let estates = EstateIron {
product: Some("gunnar".into()),
laid: vec![site_run, adopted],
};
let standing: Vec<String> = ["front", "main", "twin"]
.iter()
.flat_map(|r| [format!("gunnarrs-{r}"), format!("gunnarrs-{r}-disk")])
.collect();
let cloud = Standing {
listed: listed_rows,
standing,
};
let c = cloud_inventory_pb("gunnar", &cloud, &s.ledger, &estates);
assert!(c.available, "{}", c.error);
// Owned: the site's 6 and the adopted estate's 6 — no backup among them.
assert!(
c.owned.iter().all(|o| !o.id.starts_with("storage/bk-")),
"a backup is never an owned disk: {:?}",
c.owned
);
assert_eq!(c.owned.len(), 12, "{:?}", c.owned);
assert_eq!(c.total_bytes, 9 * 20 * GIB, "the totals are the owned rows' alone (6 listed, 3 adopted disks), no backup in them: {c:?}");
// The orphans: the stray disk, its backup, and the backup of a gone disk, by title.
let mut orphans: Vec<(&str, &str)> = c
.orphans
.iter()
.map(|o| (o.id.as_str(), o.title.as_str()))
.collect();
orphans.sort();
assert_eq!(
orphans,
vec![
(
"storage/bk-gone",
"Simple backup of gunnar-front-old system"
),
("storage/bk-stray", "Simple backup of ghost system"),
("storage/stray", "")
],
"{:?}",
c.orphans
);
assert!(
c.orphans
.iter()
.filter(|o| o.id.starts_with("storage/bk-"))
.all(|o| o.kind == "backup"),
"an orphan backup says it is a backup: {:?}",
c.orphans
);
// The backups: each under its disk's estate, in words.
let got: Vec<(&str, &str, &str)> = c
.backups
.iter()
.map(|b| (b.id.as_str(), b.estate.as_str(), b.words.as_str()))
.collect();
assert_eq!(
got,
vec![
(
"storage/bk-site-27",
"test2-gunnar-rs",
"backup of gunnar-front-test2gunnarrs, 2026-10-27"
),
(
"storage/bk-site-26",
"test2-gunnar-rs",
"backup of gunnar-front-test2gunnarrs, 2026-10-26"
),
(
"storage/bk-adopted",
"gunnar-rs",
"backup of gunnar-front, 2026-10-27"
),
("storage/bk-box", "", "backup of monetize-prod, 2026-10-27"),
],
"{:?}",
c.backups
);
assert!(
c.backups
.iter()
.all(|b| b.gib == 10 && b.created.starts_with("2026-10-2") && !b.origin.is_empty()),
"{:?}",
c.backups
);
assert_eq!(
c.backup_bytes,
4 * 10 * GIB,
"counted apart from the owned disks"
);
// Another product's panel lists the same backups and orphans: they are estates', not a product's.
let other = cloud_inventory_pb("holger", &cloud, &s.ledger, &estates);
assert_eq!(other.backups.len(), 4, "{other:?}");
assert_eq!(other.orphans.len(), 3, "{other:?}");
assert!(other.owned.is_empty(), "{other:?}");
}
}