Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-ui-server/src/api.rs

//! The console's own HTTP API: monetize's answers, JOINED and shaped for a browser.
//!
//! Refusals keep monetize's words ([`Fail`]); the one thing this layer refuses
//! on its own is an order that caps a meter its product does not declare, and
//! it names the meter — the server refuses the same thing, so the two agree.

use std::collections::{BTreeMap, HashMap};

use axum::extract::{Path, Query, State};
use axum::response::{IntoResponse, Response};
use axum::Json;
use monetize_proto as pb;
use monetize_proto::query_ledger_request::By;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::app::AppState;
use crate::grpc::Fail;

pub struct ApiError(pub Fail);

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        (
            self.0.http_status(),
            Json(json!({ "code": self.0.code, "message": self.0.message })),
        )
            .into_response()
    }
}

impl From<tonic::Status> for ApiError {
    fn from(s: tonic::Status) -> ApiError {
        ApiError(s.into())
    }
}

impl From<Fail> for ApiError {
    fn from(f: Fail) -> ApiError {
        ApiError(f)
    }
}

type Res<T> = Result<Json<T>, ApiError>;

// ── Shapes ────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MeterInfo {
    /// `enforced` | `measured` | `provisioned` | `unknown` — whether a cap on
    /// this meter is a wall the tenant hits. See `monetize_product::Enforcement`.
    #[serde(default)]
    pub enforcement: String,
    /// `tenant` | `product` | `unknown` — WHERE the number is meaningful. The
    /// second axis: `enforcement` says whether a cap is a wall, this says
    /// whether one tenant's figure may be added to another's. A `product` meter
    /// belongs on the product screen ONCE and in no per-tenant list.
    #[serde(default)]
    pub scope: String,
    pub name: String,
    pub meaning: String,
    /// **The price-list unit a cap on this meter is priced by** —
    /// `disk_gib_month` | `cpu_core_month` | `ram_gib_month` — or empty for a
    /// count that costs nothing to promise. A meter is BILLED iff the
    /// product's list carries a non-zero number under this key in the tenant's
    /// market; the console's `billed` / `not billed` words are read off that.
    #[serde(default)]
    pub unit: String,
}

fn meters(p: &pb::ProductInfo) -> Vec<MeterInfo> {
    p.meters
        .iter()
        .map(|m| MeterInfo {
            name: m.name.clone(),
            meaning: m.meaning.clone(),
            enforcement: enforcement_name(m.enforcement),
            scope: scope_name(m.scope),
            unit: m.unit.clone(),
        })
        .collect()
}

/// **What a resource unit costs the customer per month in one market**, as
/// the console reads it (`monetize::PriceList` on the wire). No list for a
/// product is free in every market; a unit absent from `units` is zero, and
/// zero is free.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PriceListJson {
    pub product: String,
    /// `SE` | `FI`.
    pub market: String,
    /// `SEK` | `EUR`.
    pub currency: String,
    /// Minor units per unit-month, keyed by `MeterInfo::unit`.
    pub units: BTreeMap<String, u64>,
}

pub fn market_name(v: i32) -> &'static str {
    match pb::Market::try_from(v) {
        Ok(pb::Market::Fi) => "FI",
        Ok(pb::Market::Dk) => "DK",
        _ => "SE",
    }
}

pub fn market_from_name(s: &str) -> Option<pb::Market> {
    match s.to_ascii_uppercase().as_str() {
        "SE" => Some(pb::Market::Se),
        "FI" => Some(pb::Market::Fi),
        "DK" => Some(pb::Market::Dk),
        _ => None,
    }
}

pub fn price_list_json(p: &pb::PriceList) -> PriceListJson {
    PriceListJson {
        product: p.product.clone(),
        market: market_name(p.market).into(),
        currency: currency_name(p.currency).into(),
        units: p.units.clone(),
    }
}

/// The deployment's one policy: the billing period and the ladder's days.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PolicyJson {
    /// `month` | `year`.
    pub period: String,
    pub grace_days: u32,
    pub retention_days: u32,
}

pub fn policy_json(p: Option<&pb::Policy>) -> PolicyJson {
    match p {
        Some(p) => PolicyJson {
            period: p.period.clone(),
            grace_days: p.grace_days,
            retention_days: p.retention_days,
        },
        // A monetize too old to say: the console shows no period rather than a guessed one.
        None => PolicyJson {
            period: String::new(),
            grace_days: 0,
            retention_days: 0,
        },
    }
}

/// Where a meter's number is meaningful, as the console spells it.
///
/// An unrecognised value is `unknown` and NOT defaulted to `tenant`, for
/// `enforcement_name`'s reason with a sharper edge: defaulting to `tenant` would
/// invite a console to add up a figure the product never said was addable, which
/// is precisely the arithmetic-on-nothing this axis exists to stop.
fn scope_name(v: i32) -> String {
    match pb::Scope::try_from(v) {
        Ok(pb::Scope::Tenant) => "tenant".into(),
        Ok(pb::Scope::Product) => "product".into(),
        Ok(pb::Scope::Unspecified) | Err(_) => "unknown".into(),
    }
}

/// The product's enforcement kind as the word the console shows beside a cap.
///
/// It matters because a cap that is a WALL and a cap that is a line item look
/// identical as a number, and the tenant cannot tell them apart from the
/// invoice. An unrecognised value is reported as unknown rather than defaulted
/// to "enforced", which would be the reassuring lie.
fn enforcement_name(v: i32) -> String {
    match pb::Enforcement::try_from(v) {
        Ok(pb::Enforcement::Enforced) => "enforced".into(),
        Ok(pb::Enforcement::Measured) => "measured".into(),
        Ok(pb::Enforcement::Provisioned) => "provisioned".into(),
        Ok(pb::Enforcement::Unspecified) | Err(_) => "unknown".into(),
    }
}

/// **The currency code beside every amount, from one writer.** `plan_json` and
/// `sale_json` both need it, and two copies of a three-arm match are two places
/// for a price to acquire the wrong unit.
///
/// An UNSPECIFIED currency reads as `SEK`, which is what this layer has always
/// done and is preserved here rather than quietly changed: on the wire a
/// `Money` always carries one (`monetize_pay::Currency` has no third arm), so the
/// fallback is only reachable from a malformed message. It is written down
/// because a default currency beside a real number is the shape of a bug, and
/// the next person should know it was a decision and not an oversight.
pub fn currency_name(v: i32) -> &'static str {
    match pb::Currency::try_from(v) {
        Ok(pb::Currency::Eur) => "EUR",
        Ok(pb::Currency::Dkk) => "DKK",
        _ => "SEK",
    }
}

/// The ladder as the console spells it.
pub fn state_name(s: pb::State) -> &'static str {
    match s {
        pb::State::Unspecified => "unknown",
        pb::State::Free => "free",
        pb::State::Paid => "paid",
        pb::State::Grace => "grace",
        pb::State::Suspended => "suspended",
        pb::State::Retention => "retention",
    }
}

pub fn state_from_name(s: &str) -> Option<pb::State> {
    Some(match s {
        "free" => pb::State::Free,
        "paid" => pb::State::Paid,
        "grace" => pb::State::Grace,
        "suspended" => pb::State::Suspended,
        "retention" => pb::State::Retention,
        _ => return None,
    })
}

/// **Does this fact's signature actually verify against monetize's key?**
///
/// The console used to answer that with `!signature.is_empty()`, defended by a
/// true store invariant: monetize-server refuses to store a fact whose non-empty
/// signature does not verify, so a non-empty signature in the store IS a verified
/// one. True, and worthless as a badge — it reports THAT THE FIELD IS POPULATED.
/// Sixty-four bytes of anything turn it green, and every path that could put
/// wrong bytes there (a restore under a different key, a hand-edited store, a
/// future write path with a bug) is precisely what the badge exists to catch. A
/// green light that cannot go red is worse than no light, because an operator
/// believes it.
///
/// So the console fetches monetize's VERIFYING key (`Entitlement.PublicKey`) and
/// checks the Ed25519 signature itself, over the same canonical form the product
/// uses — `monetize_embed::signing`, linked, not re-derived, so the console and
/// every product can never disagree about what a signature covers.
pub struct Verifier(Option<monetize_embed::VerifyingKey>);

impl Verifier {
    /// The key as monetize hands it over: 32 raw bytes. Anything else — wrong
    /// length, not a point on the curve — yields a verifier that verifies
    /// NOTHING, because the alternative is a console that shrugs and shows green.
    pub fn from_bytes(key: &[u8]) -> Verifier {
        Verifier(
            <[u8; 32]>::try_from(key)
                .ok()
                .and_then(|b| monetize_embed::VerifyingKey::from_bytes(&b).ok()),
        )
    }

    /// A verifier that holds no key and therefore verifies nothing. Used where a
    /// caller has facts but no key — the answer there is "not verified", never
    /// "signed".
    pub fn none() -> Verifier {
        Verifier(None)
    }

    pub fn verifies(&self, f: &pb::EntitlementFact) -> bool {
        let Some(key) = &self.0 else { return false };
        monetize_embed::signing::verify_fact(&fact_from_pb(f), key).is_ok()
    }
}

/// The wire fact as the Rust type its signature is computed over. A field copy,
/// not a second canonical form: the bytes signed are still
/// `monetize_embed::signing::fact_message`'s and nobody else's.
fn fact_from_pb(f: &pb::EntitlementFact) -> monetize_embed::EntitlementFact {
    monetize_embed::EntitlementFact {
        tenant: monetize_embed::TenantId(f.tenant.clone()),
        plan: f.plan.clone(),
        state: match f.state() {
            pb::State::Paid => monetize_embed::State::Paid,
            pb::State::Grace => monetize_embed::State::Grace,
            pb::State::Suspended => monetize_embed::State::Suspended,
            pb::State::Retention => monetize_embed::State::Retention,
            pb::State::Free | pb::State::Unspecified => monetize_embed::State::Free,
        },
        paid_until_unix_ms: f.paid_until_unix_ms,
        caps: f.caps.clone(),
        source: f.source.clone(),
        signature: f.signature.clone(),
        issued_unix_ms: f.issued_unix_ms,
        issued_signature: f.issued_signature.clone(),
    }
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct FactJson {
    pub tenant: String,
    /// **The order this verdict came from** — the ledger reference for a
    /// purchase, an operator's label or empty for a hand-set fact. The wire
    /// field is still `plan` (it is in the signed form gunnar verifies); the
    /// console never says that word.
    pub order: String,
    pub state: &'static str,
    pub paid_until_unix_ms: Option<u64>,
    pub caps: BTreeMap<String, u64>,
    pub source: String,
    /// **The signature was CHECKED against monetize's verifying key and passed.**
    /// Not "the field is non-empty" — see [`Verifier`]. False covers every way
    /// that check can fail, including monetize declining to name its key, because
    /// "we could not verify" and "it does not verify" are the same thing to
    /// anyone relying on the badge.
    pub signed: bool,
}

pub fn fact_json(f: &pb::EntitlementFact, v: &Verifier) -> FactJson {
    FactJson {
        tenant: f.tenant.clone(),
        order: f.plan.clone(),
        state: state_name(f.state()),
        paid_until_unix_ms: f.paid_until_unix_ms,
        caps: f.caps.clone(),
        source: f.source.clone(),
        signed: v.verifies(f),
    }
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct TenantRow {
    pub tenant: String,
    /// The order the tenant's fact came from; empty for a hand-set or absent fact.
    pub order: String,
    pub state: &'static str,
    pub paid_until_unix_ms: Option<u64>,
    pub source: String,
    /// The fact's signature verified against monetize's key. See [`Verifier`].
    pub signed: bool,
    pub meters: BTreeMap<String, u64>,
    /// `None` until the poller has read the tenant once.
    pub measured_at_unix_ms: Option<u64>,
    /// **What the tenant is ALLOWED** — the caps their signed fact names — so
    /// a list can be sorted by who is closest to their cap (`docs/v2/07-console.md` rule 5)
    /// without a round trip per row. Empty when there is no fact or the fact
    /// names none: the product's own default, which monetize does not know and
    /// the console must not render as zero.
    pub caps: BTreeMap<String, u64>,
}

/// One row per tenant, from the two lists monetize keeps apart. A tenant with
/// usage and no fact is Free (the product's own default); a fact with no usage
/// is a tenant the poller has not reached yet.
pub fn join_tenants(
    usages: &[pb::UsageReport],
    facts: &[pb::EntitlementFact],
    v: &Verifier,
) -> Vec<TenantRow> {
    let mut rows: BTreeMap<String, TenantRow> = BTreeMap::new();
    let blank = |tenant: &str| TenantRow {
        tenant: tenant.into(),
        order: String::new(),
        state: "free",
        paid_until_unix_ms: None,
        source: String::new(),
        signed: false,
        meters: BTreeMap::new(),
        measured_at_unix_ms: None,
        caps: BTreeMap::new(),
    };
    for f in facts {
        let j = fact_json(f, v);
        rows.insert(
            f.tenant.clone(),
            TenantRow {
                order: j.order,
                state: j.state,
                paid_until_unix_ms: j.paid_until_unix_ms,
                source: j.source,
                signed: j.signed,
                caps: j.caps,
                ..blank(&f.tenant)
            },
        );
    }
    for u in usages {
        let row = rows
            .entry(u.tenant.clone())
            .or_insert_with(|| blank(&u.tenant));
        row.meters = u.meters.iter().map(|m| (m.name.clone(), m.value)).collect();
        row.measured_at_unix_ms = Some(u.measured_at_unix_ms);
    }
    rows.into_values().collect()
}

/// An order may only cap a meter its product declares. The message names the meter.
pub fn validate_caps(
    product: &str,
    declared: &[MeterInfo],
    caps: &BTreeMap<String, u64>,
) -> Result<(), String> {
    match caps
        .keys()
        .find(|k| !declared.iter().any(|m| &m.name == *k))
    {
        Some(m) => Err(format!(
            "the order caps meter {m:?}, which product {product} does not declare"
        )),
        None => Ok(()),
    }
}

/// **`caps=meter=value,meter=value` on a query string**, the way `/api/quote`
/// takes an order. Bytes are bytes; no unit suffix is parsed here — the
/// console does that arithmetic where the operator can see it.
pub fn parse_caps_query(s: &str) -> Result<BTreeMap<String, u64>, String> {
    let mut out = BTreeMap::new();
    for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
        let (k, v) = part
            .split_once('=')
            .ok_or_else(|| format!("caps entry {part:?} is not meter=value"))?;
        let n: u64 = v
            .trim()
            .parse()
            .map_err(|_| format!("caps entry {part:?}: {v:?} is not a whole number"))?;
        out.insert(k.trim().to_owned(), n);
    }
    Ok(out)
}

/// **One thing a tenant is standing on**, as the console renders it.
///
/// `kind` and `detail` are computed HERE rather than in the browser, so the
/// console and anything else reading this API describe a disk the same way.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HoldingJson {
    /// The purchase that granted it.
    pub reference: String,
    /// `disk` | `server` | `compute` | `unknown`.
    pub kind: String,
    /// What it is, in words: `10 GiB on gunnar-se-sto1`.
    pub detail: String,
    /// **`true` when monetize BOUGHT it** and a purge would give it back;
    /// `false` when the pool already covered it (`Ensured::OnHand`) — real
    /// capacity the tenant has, that monetize does not own and must not return.
    pub provisioned: bool,
    /// `provider:id` when bought, empty when on hand.
    pub id: String,
}

/// **What a cloud resource IS, in words** — the console's ONE vocabulary for
/// iron, used both for what a quote would buy ([`quote_json`]) and for what a
/// tenant is already standing on ([`holding_json`]). A second phrasing for the
/// second case would let the same disk read differently on two pages.
pub fn resource_words(r: Option<&pb::Resource>) -> (&'static str, String) {
    use pb::resource::Kind;
    match r.and_then(|r| r.kind.as_ref()) {
        Some(Kind::Disk(d)) => ("disk", format!("{} GiB on {}", d.gib, d.server)),
        Some(Kind::Server(s)) => (
            "server",
            format!(
                "{} in {} ({})",
                s.plan,
                s.zone,
                if s.role() == pb::ServerRole::Replica {
                    "replica"
                } else {
                    "primary"
                }
            ),
        ),
        Some(Kind::Compute(c)) => ("compute", format!("{} on {}", c.plan, c.server)),
        // A resource kind this console does not know is reported AS unknown.
        // Rendering it as a disk would be a guess about iron somebody is paying
        // for, which is the one place a guess is worst.
        None => (
            "unknown",
            "a resource this console does not recognise".to_owned(),
        ),
    }
}

pub fn holding_json(h: &pb::Holding) -> HoldingJson {
    let (kind, detail) = resource_words(h.resource.as_ref());
    HoldingJson {
        reference: h.reference.clone(),
        kind: kind.to_owned(),
        detail,
        provisioned: !h.id.is_empty(),
        id: if h.id.is_empty() {
            String::new()
        } else {
            format!("{}:{}", h.provider, h.id)
        },
    }
}

/// **The dot on `docs/v2/07-console.md` screen 1: does this product answer?**
///
/// It is the SERVER's last observation, taken by the usage poller, and it is
/// never a call made while this page loads. Two bounds, both needed: the page
/// reads a cache and so cannot hang at all, and the poller's own call carries a
/// per-call deadline (`GunnarProduct::CALL_TIMEOUT`) so a product that accepts
/// a connection and never answers cannot freeze the dot at its last value.
///
/// `checked_at_unix_ms` is on the row deliberately: a cached answer is only
/// honest if the reader can see how old it is.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ReachabilityJson {
    /// `unknown` | `up` | `down`. **UNKNOWN IS NOT DOWN.** A server that has not
    /// finished its first poll tick has asked nobody, and painting that as
    /// either reachable or unreachable is a claim nobody made.
    pub state: String,
    /// When the state was established. 0 = never asked; the console says so
    /// rather than showing a stale-looking dot.
    pub checked_at_unix_ms: u64,
    /// The last time this product answered at all, so a console can say how long
    /// a down product has been down. 0 = never.
    pub last_up_unix_ms: u64,
    /// The plugin's own words when `state` is `down` — `docs/v2/07-console.md`'s `not
    /// connected` row, with the reason behind it.
    pub detail: String,
}

pub fn reachability_json(r: Option<&pb::Reachability>) -> ReachabilityJson {
    match r {
        Some(r) => ReachabilityJson {
            state: r.state.clone(),
            checked_at_unix_ms: r.checked_at_unix_ms,
            last_up_unix_ms: r.last_up_unix_ms,
            detail: r.detail.clone(),
        },
        // A monetize too old to report it has told us nothing, which is
        // `unknown` — not `up`, which would be a green dot this console invented.
        None => ReachabilityJson {
            state: "unknown".into(),
            checked_at_unix_ms: 0,
            last_up_unix_ms: 0,
            detail: "this monetize does not report product reachability".into(),
        },
    }
}

/// **A rate, shaped for the console — including the arm that has no rate.**
///
/// `known = false` is NOT a rate of zero. It is `monetize::trend`'s
/// "not enough history", and the console must render it as *not enough history
/// yet* rather than as a flat line. `per_week` is `None` in that arm precisely so
/// a template that forgets to check `known` renders nothing rather than a zero.
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct TrendJson {
    /// The meter the rate is OF. On the surface, never inferred by the reader.
    pub meter: String,
    /// False = not enough history. See the type doc.
    pub known: bool,
    /// The meter's units per week, signed. `None` when `known` is false —
    /// absent, not zero.
    pub per_week: Option<f64>,
    /// `firm` | `approximate` | `bursty`, empty when `known` is false.
    /// **`bursty` forbids rendering a projected date at all** (`docs/v2/07-console.md` rule 4).
    pub confidence: String,
    /// `""` for a firm figure, `"~"` otherwise — the marker `docs/v2/07-console.md` rule 4 asks a
    /// projection to carry, computed once here so four screens cannot disagree.
    pub marker: &'static str,
    /// May the console turn this into "full in N weeks"? False for `bursty`.
    pub may_project: bool,
    pub largest_step_share: f64,
    pub readings: u64,
    pub span_ms: u64,
    /// The span a weekly rate needs, so the console can say how much longer.
    pub needed_span_ms: u64,
    /// The window actually applied (`last 30 d` on the screen).
    pub window_ms: u64,
    /// FLEET only: tenants that could be measured, and tenants that could not.
    /// **`unmeasured_tenants > 0` makes `per_week` a floor, not a total** — the
    /// missing tenants are unknown and are never folded in as zero.
    pub measured_tenants: u64,
    pub unmeasured_tenants: u64,
}

pub fn trend_json(meter: &str, t: &pb::TrendResponse) -> TrendJson {
    TrendJson {
        meter: meter.to_owned(),
        known: t.enough_history,
        per_week: t.enough_history.then_some(t.per_week),
        confidence: t.confidence.clone(),
        marker: match t.confidence.as_str() {
            "firm" => "",
            _ => "~",
        },
        may_project: t.confidence == "firm" || t.confidence == "approximate",
        largest_step_share: t.largest_step_share,
        readings: t.readings,
        span_ms: t.span_ms,
        needed_span_ms: t.needed_span_ms,
        window_ms: t.window_ms,
        measured_tenants: t.measured_tenants,
        unmeasured_tenants: t.unmeasured_tenants,
    }
}

/// **One meter's fleet total and its oversell number**, shaped for the console.
///
/// `oversell` is read BEFORE `oversell_bytes`: `unmeasured` and `not_storage`
/// carry no bytes, and a reader that took the number alone would see a confident
/// zero where there is no measurement at all.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct FleetMeterJson {
    pub meter: String,
    pub tenants: u64,
    /// The sum of the latest reading, over tenants that have one.
    pub used: u64,
    /// Tenants with no reading of this meter — above zero makes `used` a floor.
    pub tenants_without_reading: u64,
    /// The sum of the caps the tenants' signed facts name.
    pub allowance: u64,
    /// Tenants whose fact names no cap here (the PRODUCT's own default, which
    /// monetize does not know) — above zero makes `allowance` a floor.
    pub tenants_without_cap: u64,
    /// `oversold` | `fits` | `unmeasured` | `not_storage`.
    pub oversell: String,
    /// Bytes oversold, or bytes of headroom. Meaningless for the other two arms.
    pub oversell_bytes: u64,
    /// The product's own reason, when `oversell` is `unmeasured`.
    pub oversell_reason: String,
    /// `used + servable` — what this fleet could grow to. **`None` when the
    /// product could not measure**: a bar with an invented denominator is worse
    /// than no bar.
    pub capacity: Option<u64>,
    /// Any number here is a floor rather than a total. The console must say so.
    pub is_a_floor: bool,
}

/// **The fleet: what a product's tenants hold, what they have been promised, and
/// what the product says it can serve.**
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct FleetJson {
    /// One entry per PER-TENANT meter. A `scope: product` meter is absent —
    /// there is nothing to add up.
    pub meters: Vec<FleetMeterJson>,
    /// False covers both silences: a product that could not be asked, and one
    /// that answered "I could not measure". `servable_reason` says which.
    pub servable_measured: bool,
    pub servable_bytes: u64,
    pub total_bytes: u64,
    pub servable_reason: String,
    /// When the POLLER last took this answer. 0 = never asked. It is deliberately
    /// not taken during this request: a page must not hang on the slowest box in
    /// the estate.
    pub servable_at_unix_ms: u64,
    /// **Whether the store can ever hold more** — `docs/v2/07-console.md` screen 2's `⚠ cannot
    /// grow`, and the paragraph behind it.
    pub growth: GrowthJson,
    /// **CLOUD: what we own, what it costs, orphans/missing** — the server's
    /// `CloudInventory`, field for field (`usage.proto`). `available: false`
    /// with `error` when the provider could not be listed; the lists are then
    /// empty because nobody asked, not because nothing is there.
    pub cloud: Value,
    /// PROD-4: the read did not come within the page's bound; this is its answer this many
    /// seconds old (the live one loads in on the next look).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stale_secs: Option<u64>,
}

/// The growth fact as the console reads it. ONE field on the fleet, two words
/// in it: `state` is what the badge keys on, `detail` is what it holds.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct GrowthJson {
    /// `sealed` | `operator_only` | `unknown`, or `""` when the product could
    /// not be measured at all — which is not `unknown`: nobody asked.
    pub state: String,
    /// The product's own remediation sentence, verbatim (gunnar's
    /// `remediation()`). Empty with an empty `state`.
    pub detail: String,
}

pub fn fleet_json(f: &pb::FleetResponse) -> FleetJson {
    FleetJson {
        stale_secs: None,
        meters: f
            .meters
            .iter()
            .map(|m| FleetMeterJson {
                meter: m.meter.clone(),
                tenants: m.tenants,
                used: m.used,
                tenants_without_reading: m.tenants_without_reading,
                allowance: m.allowance,
                tenants_without_cap: m.tenants_without_cap,
                oversell: m.oversell.clone(),
                oversell_bytes: m.oversell_bytes,
                oversell_reason: m.oversell_reason.clone(),
                capacity: m.capacity,
                is_a_floor: m.is_a_floor,
            })
            .collect(),
        servable_measured: f.servable_measured,
        servable_bytes: f.servable_bytes,
        total_bytes: f.total_bytes,
        servable_reason: f.servable_reason.clone(),
        servable_at_unix_ms: f.servable_at_unix_ms,
        growth: GrowthJson {
            state: f.growth.clone(),
            detail: f.growth_detail.clone(),
        },
        cloud: match &f.cloud {
            Some(c) => {
                let owned = |o: &pb::OwnedResource| json!({ "provider": o.provider, "id": o.id, "kind": o.kind, "gib": o.gib, "zone": o.zone, "attached_to": o.attached_to, "reference": o.reference, "tenant": o.tenant, "priced": o.priced, "monthly_minor": o.monthly_minor, "provenance": o.provenance, "estate": o.estate, "resource": o.resource, "title": o.title });
                json!({
                    "provider": c.provider, "available": c.available, "error": c.error,
                    "owned": c.owned.iter().map(owned).collect::<Vec<_>>(),
                    "orphans": c.orphans.iter().map(owned).collect::<Vec<_>>(),
                    "missing": c.missing.iter().map(|m| json!({ "provider": m.provider, "id": m.id, "reference": m.reference, "tenant": m.tenant, "resource": m.resource, "estate": m.estate })).collect::<Vec<_>>(),
                    "foreign_count": c.foreign_count, "total_bytes": c.total_bytes, "monthly_minor": c.monthly_minor, "unpriced": c.unpriced,
                    "currency": c.currency,
                    "account": c.account,
                    "backups": c.backups.iter().map(|b| json!({ "provider": b.provider, "id": b.id, "gib": b.gib, "zone": b.zone, "origin": b.origin, "server": b.server, "created": b.created, "title": b.title, "estate": b.estate, "reference": b.reference, "words": b.words })).collect::<Vec<_>>(),
                    "backup_bytes": c.backup_bytes,
                })
            }
            None => {
                json!({ "provider": "", "available": false, "error": "the server sent no cloud block", "owned": [], "orphans": [], "missing": [], "foreign_count": 0, "total_bytes": 0, "monthly_minor": 0, "unpriced": 0, "currency": "", "account": "", "backups": [], "backup_bytes": 0 })
            }
        },
    }
}

async fn fleet_of(state: &AppState, product: &str) -> Result<FleetJson, ApiError> {
    // PROD-4: the cloud is listed within the page's bound, else its last listing, aged.
    let mut u = state.monetize.usage();
    let p = product.to_owned();
    let call = async move {
        u.fleet(pb::FleetRequest { product: p })
            .await
            .map(|r| r.into_inner())
    };
    let (r, stale) =
        crate::bounded::within(&state.monetize.clouds_seen, product, "the cloud", call).await?;
    Ok(FleetJson {
        stale_secs: stale,
        ..fleet_json(&r)
    })
}

/// **The meter a product screen leads with**: the product's first `enforced`
/// meter — the first cap that is a WALL, which is the number an operator runs out
/// of. gunnar's is `pack_bytes`.
///
/// `None` when a product declares no wall at all (holger today, which polices
/// nothing). A product with no wall gets no headline rate rather than a rate of
/// whichever meter happened to sort first, because a rate whose meter nobody
/// chose is a number nobody can check.
pub fn primary_meter(p: &pb::ProductInfo) -> Option<String> {
    p.meters
        .iter()
        .find(|m| enforcement_name(m.enforcement) == "enforced")
        .map(|m| m.name.clone())
}

/// One rate from monetize, for `tenant` (empty = the product's fleet). `None`
/// when the product declares no meter to lead with — the console then shows no
/// trend line at all, which is honest, rather than a zero.
async fn trend_of(
    state: &AppState,
    product: &str,
    tenant: &str,
    meter: Option<String>,
) -> Result<Option<TrendJson>, ApiError> {
    let Some(meter) = meter else { return Ok(None) };
    let r = state
        .monetize
        .usage()
        .trend(pb::TrendRequest {
            product: product.to_owned(),
            tenant: tenant.to_owned(),
            meter: meter.clone(),
            window_ms: 0,
        })
        .await?
        .into_inner();
    Ok(Some(trend_json(&meter, &r)))
}

/// **One attempt as one line** — `docs/v2/07-console.md` rule 6, outcomes not steps.
///
/// `Sold bob 10 GiB · 220.00 SEK ✓` is this struct; the six ledger rows are
/// still reachable behind the `reference`, which is what a disclosure opens.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct SaleJson {
    /// The ledger key — what a disclosure fetches the steps with.
    pub reference: String,
    pub product: String,
    pub tenant: String,
    /// Minor units, as everything about money in this system is. Never a float.
    pub price_minor: u64,
    pub currency: &'static str,
    /// **What the provider charges us** for what this sale bought, as quoted
    /// on the same ledger row — beside the price, so a margin is a fold.
    pub cost_minor: u64,
    pub cost_currency: &'static str,
    /// **What the payment vendor kept.** `null` is "no vendor reported one",
    /// which the console must not render as a free sale — see `SaleJson::fee` in
    /// the engine. The net is not sent: it is `price_minor − fee_minor`.
    pub fee_minor: Option<u64>,
    pub fee_currency: &'static str,
    /// **The VAT this order carries**, minor units; `null` when the attempt
    /// charged nothing (declined). The vendor's figure when `tax_from_vendor`,
    /// else the recorded rate on the net price.
    pub tax_minor: Option<u64>,
    pub tax_currency: &'static str,
    pub tax_from_vendor: bool,
    /// The rate, basis points (2500 = 25 %): the vendor's for the buyer's
    /// country when it said, else the one recorded at sale time.
    pub vat_basis_points: u32,
    /// **The buyer's country** as the vendor returned it for this payment
    /// (owner 2026-10-11: Stripe decides per country, each payment is a row).
    /// Empty: the vendor did not say.
    pub customer_country: String,
    /// What the tenant is allowed after the sale — the full caps.
    pub caps: BTreeMap<String, u64>,
    /// **What was SOLD** — the delta over what they held. `Sold bob 90 GiB`.
    pub delta: BTreeMap<String, u64>,
    pub started_unix_ms: u64,
    /// The attempt's NEWEST row: what the list is ordered by.
    pub at_unix_ms: u64,
    /// Ledger rows behind this line — the disclosure's badge.
    pub steps: u32,
    /// Money was actually captured at some point. Cuts across `outcome`: a
    /// released reservation and an abandoned attempt are both `refunded` and only
    /// the second moved money.
    pub drawn: bool,
    /// `granted` | `refunded` | `declined` | `open`. **`declined` is not a
    /// refund** — nothing was locked, so rendering it as one invents a credit.
    pub outcome: String,
    pub reason: String,
    /// **The only row that is actually a task.** `docs/v2/07-console.md` floats these to NEEDS
    /// YOU; everything else is history.
    pub needs_you: bool,
}

pub fn sale_json(s: &pb::Sale) -> SaleJson {
    SaleJson {
        reference: s.reference.clone(),
        product: s.product.clone(),
        tenant: s.tenant.clone(),
        price_minor: s.price.as_ref().map(|m| m.minor).unwrap_or(0),
        currency: currency_name(s.price.as_ref().map(|m| m.currency).unwrap_or(0)),
        cost_minor: s.cost.as_ref().map(|m| m.minor).unwrap_or(0),
        cost_currency: currency_name(s.cost.as_ref().map(|m| m.currency).unwrap_or(0)),
        fee_minor: s.fee.as_ref().map(|m| m.minor),
        fee_currency: currency_name(s.fee.as_ref().map(|m| m.currency).unwrap_or(0)),
        tax_minor: s.tax.as_ref().map(|m| m.minor),
        tax_currency: currency_name(s.tax.as_ref().map(|m| m.currency).unwrap_or(0)),
        tax_from_vendor: s.tax_from_vendor,
        vat_basis_points: s.vat_basis_points,
        customer_country: s.customer_country.clone(),
        caps: s.caps.clone(),
        delta: s.delta.clone(),
        started_unix_ms: s.started_unix_ms,
        at_unix_ms: s.at_unix_ms,
        steps: s.steps,
        drawn: s.drawn,
        outcome: s.outcome.clone(),
        reason: s.reason.clone(),
        needs_you: s.needs_you,
    }
}

/// **The spend against one cloud's ceiling** — `docs/v2/07-console.md` screen 1's last lines.
/// Top level on `/api/products`, not per product: the ceiling is per cloud, so
/// the route carries a list with one entry per cloud linked.
/// The two limits are `Option` because an unset limit is NOT zero (zero means
/// refuse every purchase), and both `None` is the one dangerous state the
/// console must say in words.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct SpendJson {
    pub provider: String,
    pub currency: String,
    /// Committed EVER, per the ledger's fold — a lifetime total.
    pub spent_minor: u64,
    pub spent_this_month_minor: Option<u64>,
    pub ceiling_total_minor: Option<u64>,
    pub ceiling_monthly_minor: Option<u64>,
}

pub fn spend_json(s: &pb::CloudSpend) -> SpendJson {
    SpendJson {
        provider: s.provider.clone(),
        currency: s.currency.clone(),
        spent_minor: s.spent_minor,
        spent_this_month_minor: s.spent_this_month_minor,
        ceiling_total_minor: s.ceiling_total_minor,
        ceiling_monthly_minor: s.ceiling_monthly_minor,
    }
}

async fn sales_of(state: &AppState, product: &str, limit: u32) -> Result<Vec<SaleJson>, ApiError> {
    let r = state
        .monetize
        .ledger()
        .sales(pb::SalesRequest {
            product: product.to_owned(),
            limit,
        })
        .await?
        .into_inner();
    Ok(r.sales.iter().map(sale_json).collect())
}

/// How many folded attempts a "recently bought" list shows. `docs/v2/07-console.md` screen 2
/// shows two and a link; this is the disclosure's worth, not the page's.
pub const RECENT_SALES: u32 = 50;

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct RowJson {
    pub reference: String,
    pub seq: u32,
    pub at_unix_ms: u64,
    pub kind: String,
    pub json: String,
}

fn row_json(r: &pb::LedgerRow) -> RowJson {
    RowJson {
        reference: r.reference.clone(),
        seq: r.seq,
        at_unix_ms: r.at_unix_ms,
        kind: r.kind.clone(),
        json: r.json.clone(),
    }
}

// ── Handlers ──────────────────────────────────────────────────────────

/// **The site a money panel is for**: `?estate=<label>`. Absent: every row.
#[derive(serde::Deserialize, Default)]
pub struct SiteQuery {
    #[serde(default)]
    pub estate: String,
}

/// **The site's own vendor instance** (`[estates.<label>] vendor`, e.g.
/// `stripe/gunnar-test2`): `None` when no site was named (every row), and
/// `Some("")` for a site this monetize sells through no vendor for (no row).
///
/// Owner 2026-10-12: test2's Money page listed gunnar.rs's `stripe/gunnar`
/// sessions — the money panels took no site at all and listed the whole ONE.
pub async fn site_vendor(state: &AppState, estate: &str) -> Result<Option<String>, ApiError> {
    if estate.is_empty() {
        return Ok(None);
    }
    Ok(Some(
        list_products(state)
            .await?
            .estate_vendors
            .get(estate)
            .cloned()
            .unwrap_or_default(),
    ))
}

/// Is a row of vendor instance `vendor` this site's? Every row when no site is named.
pub fn on_site(vendor: &str, site: &Option<String>) -> bool {
    match site {
        None => true,
        Some(v) => !v.is_empty() && v == vendor,
    }
}

async fn list_products(state: &AppState) -> Result<pb::ListProductsResponse, ApiError> {
    Ok(state
        .monetize
        .products()
        .list(pb::ListProductsRequest {})
        .await?
        .into_inner())
}

/// **monetize's verifying key, fetched fresh.**
///
/// Not cached, on purpose and consistently with the rest of this layer (see
/// `grpc`: nothing is cached, monetize's reads are local redb lookups). A cached
/// key is a key that keeps verifying facts after a rotation, and the whole point
/// of the badge is that it tells the truth about the CURRENT key.
///
/// A failure PROPAGATES rather than degrading to `Verifier::none()`. If monetize
/// cannot say which key it signs with, the console cannot honestly render
/// signature status, and quietly painting every fact "not verified" would be the
/// same reassuring-lie mistake as the old badge with the colours swapped.
///
/// **Per product (G8)**: each product's facts are signed with that product's
/// key, so the badge asks for the key of the product it is painting.
async fn verifier(state: &AppState, product: &str) -> Result<Verifier, ApiError> {
    let key = state
        .monetize
        .entitlement()
        .public_key(pb::PublicKeyRequest {
            product: product.to_string(),
        })
        .await?
        .into_inner();
    Ok(Verifier::from_bytes(&key.public_key))
}

async fn product_info(state: &AppState, id: &str) -> Result<pb::ProductInfo, ApiError> {
    list_products(state)
        .await?
        .products
        .into_iter()
        .find(|p| p.id == id)
        .ok_or_else(|| {
            ApiError(Fail {
                code: "notfound".into(),
                message: format!("product {id:?} is not served by monetize"),
            })
        })
}

/// `GET /api/health` — is monetize answering, and with what.
pub async fn health(State(state): State<AppState>) -> Json<Value> {
    match list_products(&state).await {
        Ok(r) => Json(json!({
            "serving": true,
            "products": r.products.iter().map(|p| p.id.clone()).collect::<Vec<_>>(),
            "vendors": r.vendors,
            "clouds": r.clouds,
            "console": crate::VERSION_LINE,
            // Money at the vendor that nobody has placed. On the health line
            // because it is the one number that must be seen without opening
            // Activity; `null` when monetize could not be asked, which is not
            // zero.
            "paid_at_vendor_open": paid_at_vendor_open(&state).await,
        })),
        Err(e) => {
            Json(json!({ "serving": false, "error": e.0.message, "console": crate::VERSION_LINE }))
        }
    }
}

/// **`GET /api/estates`** — every estate the ONE monetize's FLEET knows
/// ([`crate::estates::read`], one call to the ONE): name, product, the boxes
/// heard calling home, `serving` when one called within the guardian's
/// bound, and the pull model's words when none did. Nobody is dialled: an
/// estate has no control to dial (ONE monetize, owner 2026-10-10).
pub async fn estates(State(state): State<AppState>) -> Result<Json<Value>, ApiError> {
    let fleet = crate::estates::read(&state.monetize).await?;
    Ok(Json(json!({ "estates": fleet })))
}

/// `GET /api/products` — one card per product with tenant counts by state.
pub async fn products(State(state): State<AppState>) -> Res<Value> {
    let listed = list_products(&state).await?;
    // RA3-1: every card's fleet read AT ONCE — each is bounded by the page rule (2 s), and read
    // one after another N products waited N bounds on a stuck cloud listing.
    let mut fleets =
        futures_util::future::join_all(listed.products.iter().map(|p| fleet_of(&state, &p.id)))
            .await
            .into_iter();
    let mut cards = Vec::new();
    for p in &listed.products {
        let verifier = verifier(&state, &p.id).await?;
        let usages = state
            .monetize
            .usage()
            .read_all(pb::ReadAllUsageRequest {
                product: p.id.clone(),
            })
            .await?
            .into_inner()
            .usages;
        let facts = state
            .monetize
            .entitlement()
            .list(pb::ListEntitlementsRequest {
                product: p.id.clone(),
            })
            .await?
            .into_inner()
            .facts;
        let rows = join_tenants(&usages, &facts, &verifier);
        let mut by_state: BTreeMap<&'static str, usize> = BTreeMap::new();
        for r in &rows {
            *by_state.entry(r.state).or_default() += 1;
        }
        // The fleet rate for this product's headline meter. One extra call per
        // product, on the SERVER's stored series — the console never folds it
        // itself (`docs/v2/06-data-model.md` §6.1: no measurement taken by the console).
        let trend = trend_of(&state, &p.id, "", primary_meter(p)).await?;
        // `184 / 300 GiB` on `docs/v2/07-console.md` screen 1, and the oversell behind it.
        // A card prints the account's total; its rows are the operator's Iron page's (MAP-SEG 1).
        let mut fleet = fleets.next().expect("one fleet read per product")?;
        fleet.cloud = crate::iron::card_cloud(&fleet.cloud);
        cards.push(json!({
            "id": p.id, "meters": meters(p), "tenants": rows.len(), "by_state": by_state,
            "trend": trend, "fleet": fleet,
            // The dot. `docs/v2/07-console.md` screen 1.
            "reachable": reachability_json(p.reachable.as_ref()),
            // **And whether the PAID DOOR is there**, which the dot cannot say:
            // a community appliance is perfectly reachable and cannot enforce a
            // single entitlement. `unproven` is carried through as itself —
            // folding it into either answer would make the claim the probe
            // exists to test.
            "metered_door": json!({
                "state": p.metered_door.as_ref().map(|d| d.state.clone()).unwrap_or_else(|| "unproven".into()),
                "checked_at_unix_ms": p.metered_door.as_ref().map(|d| d.checked_at_unix_ms).unwrap_or(0),
                "detail": p.metered_door.as_ref().map(|d| d.detail.clone()).unwrap_or_else(|| "this monetize does not report a metered door".into()),
            }),
        }));
    }
    // The spend lines: EVERY cloud's, never only the first. Until 2026-09-14
    // this took `spend.first()`, and with `sandbox` and `upcloud` both linked
    // the page showed the sandbox's 10 EUR ceiling while the cloud that spends
    // real money was not on the screen at all. Empty on a box without a cloud,
    // and the console then draws no line rather than a cap of nothing.
    // `default_cloud` is the cloud a sale provisions on, empty when none is.
    let spend: Vec<SpendJson> = listed.spend.iter().map(spend_json).collect();
    Ok(Json(json!({
        "products": cards, "vendors": listed.vendors, "clouds": listed.clouds, "spend": spend, "default_cloud": listed.default_cloud,
        // THE TERMS: what a unit costs the customer per product per market (no
        // list = free), the one policy, and where each product's disks attach.
        "prices": listed.prices.iter().map(price_list_json).collect::<Vec<_>>(),
        "policy": policy_json(listed.policy.as_ref()),
        "hosts": listed.hosts,
    })))
}

/// **How long a tenant must have been unseen before the console calls it
/// silent: 14 days.**
///
/// Two weeks, so an ordinary holiday is not an alarm and a customer who has
/// actually stopped is one. The same number gunnar's own dashboard band uses;
/// it is a constant and not a setting because a threshold an operator can tune
/// is a threshold two surfaces will disagree about.
pub const SILENT_AFTER_DAYS: u64 = 14;

/// **How far back the FRICTION list looks: 7 days.**
///
/// A week, because friction is a thing an operator acts on THIS week — a
/// customer who was refused twice last month and not since has a story that
/// has already ended, and putting them on a task list is how a task list comes
/// to be ignored.
pub const FRICTION_DAYS: u64 = 7;

/// `?estate=` on [`tenants`]: the estate whose page asks (MAP-SEG 1).
#[derive(Debug, Default, Deserialize)]
pub struct EstateQuery {
    #[serde(default)]
    pub estate: String,
}

/// `GET /api/products/{id}/tenants[?estate=<estate>]` — ONE estate's part of the product
/// (MAP-SEG 1, owner 2026-10-26: the Map of test2-gunnar-rs listed every estate's iron).
/// Its tenants ([`crate::estates::tenant_on`]) and their sales, and its iron
/// ([`crate::iron::estate_cloud`]); no `estate`: the rows of no ONE estate. The account's
/// iron, whole, is the operator's `GET /api/iron`.
pub async fn tenants(
    State(state): State<AppState>,
    Path(id): Path<String>,
    Query(q): Query<EstateQuery>,
) -> Res<Value> {
    let p = product_info(&state, &id).await?;
    let usages = state
        .monetize
        .usage()
        .read_all(pb::ReadAllUsageRequest {
            product: id.clone(),
        })
        .await?
        .into_inner()
        .usages;
    let facts = state
        .monetize
        .entitlement()
        .list(pb::ListEntitlementsRequest {
            product: id.clone(),
        })
        .await?
        .into_inner()
        .facts;
    let trend = trend_of(&state, &id, "", primary_meter(&p)).await?;
    let mut fleet = fleet_of(&state, &id).await?;
    // `RECENTLY BOUGHT` on `docs/v2/07-console.md` screen 2 — outcomes, one line per attempt.
    let mut sales = sales_of(&state, &id, RECENT_SALES).await?;
    let mut rows = join_tenants(&usages, &facts, &verifier(&state, &id).await?);
    let mut own = false;
    if !q.estate.trim().is_empty() {
        let one = fleet_estate(&state, q.estate.trim()).await?;
        let sold = book(&state).await?.sold().contains(&one.name);
        own = !sold;
        rows.retain(|r| crate::estates::tenant_on(&one, sold, &r.tenant));
        sales.retain(|s| crate::estates::tenant_on(&one, sold, &s.tenant));
    }
    fleet.cloud = crate::iron::estate_cloud_of(&fleet.cloud, q.estate.trim(), own);
    let (silent, no_flow_data) = silent_tenants(&rows, now_ms());
    let friction = friction_tenants(&state, &id, &rows).await?;
    Ok(Json(json!({
        "product": id, "meters": meters(&p), "tenants": rows,
        "trend": trend, "fleet": fleet, "sales": sales,
        // **The two lists that no stock meter could ever produce.**
        //
        // SILENT is exact and costs nothing: it is read off the two
        // `last_*_unix_ms` meters the poller already stored, and gunnar computes
        // those over its WHOLE kept window, so a tenant with a today of zeroes
        // and a push six weeks ago is six weeks quiet rather than silent
        // forever.
        //
        // FRICTION is folded from the readings THIS SERVER holds
        // (`monetize::flow`), never by asking the product once per tenant — a
        // fleet page that made N calls would hang on the slowest box in the
        // estate, which is the failure the reachability dot exists to avoid.
        // The consequence is deliberate and is named on the screen: the fleet
        // page says WHO and HOW MANY, and the reason breakdown lives one click
        // away on the tenant's Activity panel, where the day rows carry it.
        "silent_after_days": SILENT_AFTER_DAYS,
        "silent": silent,
        // **The tenants NOBODY MEASURED, by name, kept out of `silent`.**
        //
        // A product that does not report the two `last_*_unix_ms` meters says
        // nothing about whether its tenants are being used. Folding that into
        // SILENT would publish `never seen` about a customer on the strength of
        // a gap in a plugin — and `quiet_for_ms` would rank it `u64::MAX`, at
        // the TOP of the page, above every measured finding.
        //
        // Not dropped either, which would make a whole product's tenants
        // disappear from the page with no explanation. They are named, so the
        // operator reads the true sentence — *we do not know about these* —
        // instead of a comfortable one or a frightening one.
        "no_flow_data": no_flow_data,
        "friction_days": FRICTION_DAYS,
        "friction": friction,
    })))
}

/// **Tenants nobody has pushed to or fetched from for [`SILENT_AFTER_DAYS`]**,
/// longest first.
///
/// `quiet_ms` is `u64::MAX` for a tenant that has NEVER been seen, which is how
/// it sorts above one last seen a year ago; `quiet_days` is `null` for the same
/// tenant, because "never" has no number of days and a console must not be
/// handed one to render as a date. `monetize_product::flow::Quiet` is the one writer of
/// that distinction.
///
/// # A tenant nobody MEASURED is not a silent tenant
///
/// Returns TWO lists, and the second is the point: the tenants whose product
/// reports **neither** `last_*_unix_ms` meter, named and kept OUT of the first.
///
/// This function used to say the opposite, in a comment that read *"A tenant the
/// poller has never read carries no `last_*` meter at all and therefore reads as
/// never seen — which is right"*. It was not right. `flow::last_seen` folded an
/// absent meter into a meter reading `0` with `.unwrap_or(0)`, so a product that
/// does not count flow at all — an older gunnar, the `Admin.StorageInfo`
/// fallback path, a tenant the poller has never read — arrived here as
/// `Quiet::Never`, was rendered `never seen`, and was sorted to the **top** of
/// the page by `quiet_for_ms`'s `u64::MAX`. The most urgent-looking row on the
/// fleet page was a claim about a customer made from a gap in a plugin.
///
/// The tenant page had already settled it the other way — it draws *"this
/// product does not report flow"* rather than a last-seen line when neither key
/// is present. Two views of one fact disagreed. `flow::Quiet::NoData` now
/// carries the distinction in the type, `at_least_days` is false for it, and
/// this list cannot contain one by construction.
///
/// A tenant whose meters ARE reported and read `0` is still `Quiet::Never` and
/// still belongs here: that is a measurement, and "nothing has ever moved
/// through this account" is exactly what the list is for.
///
/// `quiet_ms` is `u64::MAX` for such a tenant, which is how it sorts above one
/// last seen a year ago; `quiet_days` is `null` for it, because "never" has no
/// number of days and a console must not be handed one to render as a date.
/// `monetize_product::flow::Quiet` is the one writer of both distinctions.
fn silent_tenants(rows: &[TenantRow], now_unix_ms: u64) -> (Vec<Value>, Vec<String>) {
    let usage_of = |row: &TenantRow| monetize_product::Usage {
        meters: row.meters.clone(),
        measured_at_unix_ms: row.measured_at_unix_ms.unwrap_or(0),
    };

    // The tenants nobody measured, in a stable order so the line on the page
    // does not reshuffle between polls.
    let mut no_flow_data: Vec<String> = rows
        .iter()
        .filter(|row| monetize_product::flow::last_seen(&usage_of(row)).is_none())
        .map(|row| row.tenant.clone())
        .collect();
    no_flow_data.sort_unstable();

    let mut quiet: Vec<(u64, Option<u64>, &TenantRow)> = rows
        .iter()
        .map(|row| (monetize_product::flow::last_seen(&usage_of(row)), row))
        .map(|(seen, row)| {
            (
                monetize_product::flow::quiet_for(seen, now_unix_ms),
                seen,
                row,
            )
        })
        // `NoData` is false here, so an unmeasured tenant cannot reach the list.
        .filter(|(quiet, _, _)| quiet.at_least_days(SILENT_AFTER_DAYS))
        .map(|(quiet, seen, row)| {
            (
                monetize_product::flow::quiet_for_ms(seen, now_unix_ms),
                quiet.days(),
                row,
            )
        })
        .collect();
    quiet.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.2.tenant.cmp(&b.2.tenant)));

    let silent = quiet
        .into_iter()
        .map(|(quiet_ms, days, row)| {
            json!({ "tenant": row.tenant, "state": row.state, "quiet_ms": quiet_ms, "quiet_days": days })
        })
        .collect();
    (silent, no_flow_data)
}

/// **Tenants that met a door in the last [`FRICTION_DAYS`] days**, most refused
/// first.
///
/// Folded from `Usage.History` — the readings this server already stored — and
/// never by asking the product per tenant. `refused_today` is a DAILY counter
/// and the fold is `monetize_product::flow::window_total`, which takes each UTC day's
/// peak rather than summing readings: a tenant refused once at nine in the
/// morning appears in every one of that day's remaining polls, and a naive sum
/// would report a number proportional to the poll rate.
///
/// A tenant whose history could not be read is left OUT rather than reported as
/// frictionless: this is a list of accounts to act on, and a silent omission is
/// better than a claim nobody measured. The read failure is already visible on
/// the page as the product's reachability.
async fn friction_tenants(
    state: &AppState,
    product: &str,
    rows: &[TenantRow],
) -> Result<Vec<Value>, ApiError> {
    let now = now_ms();
    let since = now.saturating_sub(FRICTION_DAYS * 24 * 60 * 60 * 1000);
    let mut out: Vec<(u64, Value)> = Vec::new();
    for row in rows {
        let Ok(answer) = state
            .monetize
            .usage()
            .history(pb::UsageHistoryRequest {
                product: product.to_owned(),
                tenant: row.tenant.clone(),
                since_unix_ms: since,
                limit: 0,
            })
            .await
        else {
            continue;
        };
        let readings: Vec<monetize_product::Usage> = answer
            .into_inner()
            .readings
            .iter()
            .map(|u| monetize_product::Usage {
                meters: u.meters.iter().map(|m| (m.name.clone(), m.value)).collect(),
                measured_at_unix_ms: u.measured_at_unix_ms,
            })
            .collect();
        let refused = monetize_product::flow::window_total(&readings, "refused_today", since, now);
        if refused == 0 {
            continue;
        }
        out.push((
            refused,
            json!({ "tenant": row.tenant, "state": row.state, "refused": refused }),
        ));
    }
    out.sort_by_key(|(refused, _)| std::cmp::Reverse(*refused));
    Ok(out.into_iter().map(|(_, v)| v).collect())
}

/// **How many days of activity the console asks for: 30.**
///
/// The window `docs/v2/07-console.md` quotes beside every rate, and the one the tenant page's
/// Activity panel draws. A constant rather than a query parameter because the
/// window is ON THE SCREEN beside the bars — "last 30 days" — and a window the
/// caller could change would be a caption that could be wrong.
pub const ACTIVITY_DAYS: u64 = 30;

/// `GET /api/tenants/{product}/{tenant}/activity` — **what the tenant has been
/// DOING, day by day.**
///
/// A passthrough to the product, through monetize-server's own passthrough.
/// Nothing is folded, nothing is cached and nothing is stored: the product is
/// the only side that counts flow, and a copy on this side would be a second
/// series to go stale.
///
/// **A refusal is a refusal and never an empty series.** The panel renders an
/// empty `days` as *nothing moved in this window*; a product that is merely
/// unreachable must not be able to say that about a working customer. So the
/// error is passed up, and the browser draws the failure box with the product's
/// own words in it.
///
/// `retained_days` rides along so the panel can tell *nothing happened* from
/// *this box does not remember that far back* — the two readings of an empty
/// series, which look identical without it.
pub async fn activity(
    State(state): State<AppState>,
    Path((product, tenant)): Path<(String, String)>,
) -> Res<Value> {
    product_info(&state, &product).await?;
    let now = now_ms();
    let out = state
        .monetize
        .usage()
        .activity(pb::ActivityRequest {
            product: product.clone(),
            tenant: tenant.clone(),
            since_unix_ms: now.saturating_sub(ACTIVITY_DAYS * 24 * 60 * 60 * 1000),
            until_unix_ms: now,
        })
        .await?
        .into_inner();
    Ok(Json(json!({
        "product": product,
        "tenant": tenant,
        "days_asked": ACTIVITY_DAYS,
        "retained_days": out.retained_days,
        "days": out.days.iter().map(activity_day_json).collect::<Vec<_>>(),
    })))
}

/// This process's wall clock, in Unix milliseconds — only ever used to say how
/// far back to ask the product for. The DAYS themselves are the product's, and
/// nothing here re-derives one.
fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// One day of flow, as the console reads it: the day, and the product's own
/// meters as a plain object.
///
/// A map and not a struct of named fields, exactly as `TenantRow::meters` is:
/// the names are the PRODUCT's, this server serves several products, and a
/// struct here would be gunnar's vocabulary hard-coded into a shape holger has
/// to fit into. A meter that is absent was not counted and is absent here too.
fn activity_day_json(day: &pb::ActivityDay) -> Value {
    let meters: BTreeMap<&str, u64> = day
        .meters
        .iter()
        .map(|m| (m.name.as_str(), m.value))
        .collect();
    json!({ "day_unix_ms": day.day_unix_ms, "meters": meters })
}

/// `GET /api/tenants/{product}/{tenant}` — the latest reading, the fact, the
/// ledger rows of its references, what the tenant stands on, and the product's
/// price lists for the order form.
///
/// The LATEST reading, because that is what this page draws bars from. It is not
/// all monetize has: `Usage.History` (`usage.proto`) answers a window of
/// readings per tenant. This comment used to say there was no history RPC, which
/// was true for a few hours and then was not, and a sentence like that is worse
/// than none — it is read as a reason not to look.
pub async fn tenant(
    State(state): State<AppState>,
    Path((product, tenant)): Path<(String, String)>,
) -> Res<Value> {
    let listed = list_products(&state).await?;
    let p = listed
        .products
        .iter()
        .find(|x| x.id == product)
        .cloned()
        .ok_or_else(|| {
            ApiError(Fail {
                code: "notfound".into(),
                message: format!("product {product:?} is not served by monetize"),
            })
        })?;
    let usage = state
        .monetize
        .usage()
        .read(pb::ReadUsageRequest {
            product: product.clone(),
            tenant: tenant.clone(),
        })
        .await?
        .into_inner()
        .usage;
    let got = state
        .monetize
        .entitlement()
        .get(pb::GetEntitlementRequest {
            product: product.clone(),
            tenant: tenant.clone(),
        })
        .await?
        .into_inner();
    let (fact, pending_push) = (got.fact, got.pending_push);
    let ledger = state
        .monetize
        .ledger()
        .query(pb::QueryLedgerRequest {
            by: Some(By::Tenant(pb::TenantQuery {
                product: product.clone(),
                tenant: tenant.clone(),
            })),
        })
        .await?
        .into_inner();
    // WHAT THE TENANT IS STANDING ON. The usage bars say how much of a quota is
    // used; this says what iron is under the quota — and the two together are
    // what an operator needs before deciding to sell someone more.
    let holdings = state
        .monetize
        .entitlement()
        .held(pb::HeldRequest {
            product: product.clone(),
            tenant: tenant.clone(),
        })
        .await?
        .into_inner()
        .holdings;
    let verifier = verifier(&state, &product).await?;
    let row = join_tenants(usage.as_slice(), fact.as_slice(), &verifier).pop();
    // `docs/v2/07-console.md` screen 3's `1.4 GiB/wk` line, folded on the server over the series
    // the poller stored.
    let trend = trend_of(&state, &product, &tenant, primary_meter(&p)).await?;
    Ok(Json(json!({
        "product": product, "tenant": tenant, "meters": meters(&p), "trend": trend,
        "row": row, "fact": fact.as_ref().map(|f| fact_json(f, &verifier)),
        // THE PRODUCT HAS NOT ACKNOWLEDGED THE FACT ABOVE: the poller is still
        // pushing it, or the product refused it and an operator must decide.
        "pending_push": pending_push.as_ref().map(|p| json!({
            "since_unix_ms": p.since_unix_ms, "attempts": p.attempts, "last_error": p.last_error,
        })),
        "rows": ledger.rows.iter().map(row_json).collect::<Vec<_>>(), "references": ledger.references,
        "holdings": holdings.iter().map(holding_json).collect::<Vec<_>>(),
        // THIS product's price lists, one per market, and the policy: what the
        // order form prices a delta by, and what the USING words are read off.
        "prices": listed.prices.iter().filter(|l| l.product == product).map(price_list_json).collect::<Vec<_>>(),
        "policy": policy_json(listed.policy.as_ref()),
        // The vendors THIS server has linked, so the order form offers only what
        // a purchase could actually use. A console that listed `stripe` on a
        // server built without it would send an operator into a NotFound.
        "vendors": listed.vendors,
    })))
}

/// `GET /api/quote?product=&tenant=&caps=meter=value,…&market=SE` — **what an
/// order would come to, before any money moves.** The price from the list
/// ("free" when 0), the cost the provider would charge us, the delta over what
/// the tenant holds, the iron it buys, and whether it can be served with the
/// refusal in monetize's own words when not. Pure: monetize reserves nothing
/// and writes nothing. A read, so a viewer's.
pub async fn quote(
    State(state): State<AppState>,
    Query(q): Query<HashMap<String, String>>,
) -> Res<QuoteJson> {
    let product = q.get("product").cloned().unwrap_or_default();
    let tenant = q.get("tenant").cloned().unwrap_or_default();
    let caps = parse_caps_query(q.get("caps").map(String::as_str).unwrap_or(""))
        .map_err(|m| ApiError(Fail::invalid(m)))?;
    // `renew=1` asks for a RENEWAL — another period of what the tenant already
    // holds — instead of an order. It names no caps and no start: monetize
    // refuses a renewal that carries either, rather than ignoring them, because
    // a request that says both things is not one whose meaning we get to pick.
    let renew = matches!(q.get("renew").map(String::as_str), Some("1" | "true"));
    let bundle = q.get("bundle").cloned().unwrap_or_default();
    let p = product_info(&state, &product).await?;
    validate_caps(&product, &meters(&p), &caps).map_err(|m| ApiError(Fail::invalid(m)))?;
    let market = match q
        .get("market")
        .map(String::as_str)
        .filter(|m| !m.is_empty())
    {
        Some(m) => market_from_name(m)
            .ok_or_else(|| ApiError(Fail::invalid(format!("market {m:?} is not SE or FI"))))?
            as i32,
        None => 0,
    };
    let out = state
        .monetize
        .purchase()
        // No actor ticket, ever: this console is the OPERATOR's, authenticated
        // by monetize's bearer, and the tenant path is the product's own
        // console asking on a customer's behalf. An operator does not need the
        // appliance's permission to renew somebody, and an empty ticket is
        // exactly the path this console has always taken.
        // **`bundle=<id>` quotes ONE ROW OF THE SHOP WINDOW**, by the id
        // `/api/bundles` handed out, resolved by monetize into exactly the caps
        // a typed order would have named. It exists so that "would you sell me
        // this?" can be asked of the PURCHASE path — the one the Buy button
        // takes — and not only of the window that advertises it. Two doors, one
        // answer, and the day they disagree is the day a customer is shown a
        // product that cannot be delivered. `caps` must be empty alongside it;
        // monetize refuses a request that names both rather than letting one
        // win silently.
        .quote(pb::QuoteRequest {
            product,
            tenant,
            caps,
            market,
            starts_unix_ms: 0,
            renew,
            actor_ticket: Vec::new(),
            bundle,
        })
        .await?
        .into_inner();
    Ok(Json(quote_json(&out)))
}

/// `GET /api/bundles?product=&tenant=&market=` — **THE SHOP WINDOW, READ.**
///
/// The same `Purchase.Bundles` the customer's own console draws its offers
/// from, answered here so that an operator — and the live bench — can see what
/// this deployment is actually WILLING TO SELL this tenant, without a browser,
/// without a session on the appliance and without pressing anything.
///
/// **It spends nothing.** One gRPC read; monetize prices the list purely and
/// asks `can_serve` through the SAME `quote_order` a real order would run, and
/// neither half reserves, writes, dials a vendor or moves a byte. It is on the
/// viewer's floor for exactly that reason, beside `/api/quote`.
///
/// # Why this route exists at all (2026-09-20)
///
/// Because nothing outside a browser could read the shop. The defect that made
/// it necessary: a live appliance offered 100 GiB and 50 GiB at full price on a
/// box whose `/data` could serve 37.8 GiB, and monetize had ALREADY written
/// both refusals to its own log before the page was drawn. The verdict existed;
/// nothing automated could ask for it. `xtask bench live`'s only money row (F9)
/// orders a real growth for real SEK, so it is opt-in, fenced and had never
/// been run — and the free question, *what would you sell me right now*, had no
/// door to ask through.
///
/// Now it has one, and it costs 0 GiB and 0 SEK.
///
/// `can_serve` and `reason` are carried EXACTLY as monetize wrote them. This
/// console does not recompute the verdict, does not summarise the sentence and
/// does not drop the refused rows: an operator's window shows the whole
/// declared shop and says which of it can be delivered, which is the question
/// an operator has. Dropping the undeliverable rows is the CUSTOMER console's
/// job, on its own surface, where a dead card is a lie about a product.
pub async fn bundles(
    State(state): State<AppState>,
    Query(q): Query<HashMap<String, String>>,
) -> Res<Value> {
    let product = q.get("product").cloned().unwrap_or_default();
    let tenant = q.get("tenant").cloned().unwrap_or_default();
    let market = match q
        .get("market")
        .map(String::as_str)
        .filter(|m| !m.is_empty())
    {
        Some(m) => market_from_name(m)
            .ok_or_else(|| ApiError(Fail::invalid(format!("market {m:?} is not SE or FI"))))?
            as i32,
        None => 0,
    };
    let out = state
        .monetize
        .purchase()
        // No actor ticket: the OPERATOR path, byte for byte as on `/api/quote`
        // — this console is authenticated by monetize's own bearer and the
        // ticket exists to stop a STRANGER reading a tenant's standing caps
        // back out of a price.
        .bundles(pb::ListBundlesRequest {
            product,
            tenant,
            market,
            actor_ticket: Vec::new(),
        })
        .await?
        .into_inner();
    let rows: Vec<Value> = out
        .bundles
        .iter()
        .map(|b| {
            json!({
                "id": b.id,
                "label": b.label,
                // WORDS, never minor units — monetize's own rendering, the same
                // string the customer's button carries.
                "price_words": b.price,
                "period": b.period,
                "delta": b.delta,
                "can_serve": b.can_serve,
                "reason": b.reason,
            })
        })
        .collect();
    Ok(Json(json!({
        "bundles": rows,
        // The two counts, so a reader does not have to fold the list to learn
        // the one thing that matters about a shop: how much of it is real.
        "declared": rows.len(),
        "deliverable": out.bundles.iter().filter(|b| b.can_serve).count(),
        // Still being read at the cloud (`ListBundlesResponse.reading`): the
        // empty list above is not an answer yet. Carried, not interpreted.
        "reading": out.reading,
    })))
}

/// **A quote, shaped for the console.** Every money figure in minor units with
/// its currency beside it; the iron in the same words the tenant page uses for
/// what a tenant already stands on.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct QuoteJson {
    /// The ledger key the order would run under.
    pub reference: String,
    /// What the customer pays for the delta over one period. 0 is free.
    pub price_minor: u64,
    pub currency: &'static str,
    /// What the provider charges us for the delta's iron over the period.
    pub cost_minor: u64,
    pub cost_currency: &'static str,
    pub current: BTreeMap<String, u64>,
    pub caps: BTreeMap<String, u64>,
    pub delta: BTreeMap<String, u64>,
    /// The iron the delta buys, in words: `90 GiB on gunnar-se-sto1`.
    pub resources: Vec<HoldingJson>,
    pub can_serve: bool,
    /// monetize's refusal, verbatim, when `can_serve` is false.
    pub reason: String,
    /// `month` | `year`.
    pub period: String,
}

pub fn quote_json(q: &pb::QuoteResponse) -> QuoteJson {
    QuoteJson {
        reference: q.reference.clone(),
        price_minor: q.price.as_ref().map(|m| m.minor).unwrap_or(0),
        currency: currency_name(q.price.as_ref().map(|m| m.currency).unwrap_or(0)),
        cost_minor: q.cost.as_ref().map(|m| m.minor).unwrap_or(0),
        cost_currency: currency_name(q.cost.as_ref().map(|m| m.currency).unwrap_or(0)),
        current: q.current.clone(),
        caps: q.caps.clone(),
        delta: q.delta.clone(),
        resources: q
            .resources
            .iter()
            .map(|r| {
                let (kind, detail) = resource_words(Some(r));
                HoldingJson {
                    reference: q.reference.clone(),
                    kind: kind.to_owned(),
                    detail,
                    provisioned: false,
                    id: String::new(),
                }
            })
            .collect(),
        can_serve: q.can_serve,
        reason: q.reason.clone(),
        period: q.period.clone(),
    }
}

/// `POST /api/purchase` — **place an order**: the console's only door to the
/// two-phase transaction monetize exists for.
///
/// An order is `{product, tenant, caps}`: what the tenant is to be ALLOWED. The
/// delta over what they hold is what monetize prices (from the list; free with
/// none) and buys — a tenant on 10 GiB who orders 100 is sold the 90 they lack
/// (MEASURED before the delta existed: 110 GiB held and billed where 100 was
/// asked for). A downgrade, a product that cannot deliver the caps, a box past
/// its ceiling, and a price nobody linked can take are monetize's refusals to
/// make, before any money moves, and they arrive here verbatim (`Fail` keeps
/// monetize's own words) — "gunnar's /data cannot grow" is the sentence the
/// operator has to read out to the customer.
///
/// Behind a fresh touch (`needs_step_up`: every POST under `/api/` is), which is right:
/// this one spends the tenant's money and Vetra's, in that order. What the BFF
/// adds is the shape a browser needs: `starts_unix_ms` omitted means "now", so
/// a console need not agree with the server about the clock; `vendor` omitted
/// means the only one linked that serves the market; `market` omitted means the
/// deployment's default.
pub async fn start_purchase(
    State(state): State<AppState>,
    Json(body): Json<StartPurchase>,
) -> Res<Value> {
    let p = product_info(&state, &body.product).await?;
    validate_caps(&body.product, &meters(&p), &body.caps)
        .map_err(|m| ApiError(Fail::invalid(m)))?;
    let product = body.product.clone();
    let market = match body.market.as_deref().filter(|m| !m.is_empty()) {
        Some(m) => market_from_name(m)
            .ok_or_else(|| ApiError(Fail::invalid(format!("market {m:?} is not SE or FI"))))?
            as i32,
        None => 0,
    };
    let out = state
        .monetize
        .purchase()
        .start(pb::StartPurchaseRequest {
            product: body.product,
            tenant: body.tenant,
            caps: body.caps,
            vendor: body.vendor.unwrap_or_default(),
            payer: body.payer.unwrap_or_default(),
            // 0 means "now" on the wire; the server owns the clock.
            starts_unix_ms: body.starts_unix_ms.unwrap_or(0),
            market,
            renew: body.renew.unwrap_or(false),
            // The operator path — see the quote above.
            actor_ticket: Vec::new(),
            bundle: body.bundle.clone().unwrap_or_default(),
            users: None,
        })
        .await?
        .into_inner();
    let verifier = verifier(&state, &product).await?;
    Ok(Json(purchase_json(&out, &verifier)))
}

/// `GET /api/purchase/{reference}` — where a started purchase stands.
///
/// The reference is `<product>/<tenant>/<YYYY-MM-DD>` and contains slashes, so
/// it arrives as the rest of the path rather than one segment.
pub async fn purchase_status(
    State(state): State<AppState>,
    Path(reference): Path<String>,
) -> Res<Value> {
    let product = reference.split('/').next().unwrap_or_default().to_string();
    let out = state
        .monetize
        .purchase()
        .status(pb::PurchaseStatusRequest {
            reference,
            ..Default::default()
        })
        .await?
        .into_inner();
    let verifier = verifier(&state, &product).await?;
    Ok(Json(json!({
        "state": purchase_state_name(out.state),
        "reference": out.reference,
        "fact": out.fact.as_ref().map(|f| fact_json(f, &verifier)),
        "reason": out.reason,
        // Still live, and only while the attempt is open: the operator who
        // reopens a PENDING purchase because the customer says the link never
        // arrived is the reason this is on the status read and not only on the
        // reply to `Start`.
        "redirect_url": out.redirect_url,
        // The ledger rows behind the verdict, oldest first — the timeline the
        // console already renders for a reference.
        "rows": out.rows.iter().map(row_json).collect::<Vec<_>>(),
    })))
}

/// What the console posts to place an order. Everything but the order itself
/// is optional, so the common call is three fields.
#[derive(Debug, serde::Deserialize)]
pub struct StartPurchase {
    pub product: String,
    pub tenant: String,
    /// What the tenant is to be ALLOWED, per meter. A meter not named carries forward.
    pub caps: BTreeMap<String, u64>,
    #[serde(default)]
    pub vendor: Option<String>,
    #[serde(default)]
    pub payer: Option<String>,
    /// `SE` | `FI`; the deployment's default when omitted.
    #[serde(default)]
    pub market: Option<String>,
    #[serde(default)]
    pub starts_unix_ms: Option<u64>,
    /// **Sell a RENEWAL** — another period of what the tenant already holds —
    /// rather than an order. `caps` must then be empty and `starts_unix_ms`
    /// absent; monetize refuses a renewal that carries either rather than
    /// ignoring them.
    ///
    /// This is the door an operator sells a renewal through today, and it is
    /// the door the Retention refusal routes to: `Transaction::renewal` will
    /// not sell a tenant the ladder has carried past retention, because an
    /// operator may already have handed their iron back — so an operator sells
    /// that one here, having looked.
    #[serde(default)]
    pub renew: Option<bool>,
    /// **Buy a whole OFFER instead of naming numbers** — a bundle id from
    /// `/api/bundles`, which is what the shop window already shows.
    ///
    /// `Purchase.Start` has honoured this since the shop window existed
    /// (`caps_of_bundle`, services/purchase.rs:660): the bundle's `cap_delta`
    /// is added to what the tenant holds, so a caller never does the
    /// arithmetic. This console sent `bundle: String::new()` unconditionally,
    /// so the only way through it was the typed form — two boxes, a meter name
    /// and a byte count. That is a debugging form, and an operator asked for a
    /// BUTTON.
    ///
    /// `caps` may be sent beside it and the server folds the two; sending
    /// neither is what it refuses.
    #[serde(default)]
    pub bundle: Option<String>,
}

fn purchase_json(o: &pb::StartPurchaseResponse, v: &Verifier) -> Value {
    json!({
        "reference": o.reference,
        "state": purchase_state_name(o.state),
        "handle_vendor": o.handle_vendor,
        "handle_id": o.handle_id,
        // Empty for a vendor with nowhere to send the customer — an invoice is
        // paid from the customer's own bank, and a PAID purchase is finished.
        // Where it is non-empty it is the VENDOR's own URL when the vendor mints
        // one per order, and the configured template otherwise.
        "redirect_url": o.redirect_url,
        "reason": o.reason,
        // Present only when PAID: the signed verdict the product now holds, so
        // the console can show the outcome without a second round trip.
        "fact": o.fact.as_ref().map(|f| fact_json(f, v)),
    })
}

/// The purchase state as the word the console shows. An unknown number is
/// reported AS unknown rather than defaulted to something reassuring — a
/// console that renders a state it does not understand as "PAID" is worse than
/// one that says it does not know.
fn purchase_state_name(v: i32) -> String {
    match pb::PurchaseState::try_from(v) {
        Ok(s) => format!("{s:?}").to_ascii_uppercase(),
        Err(_) => format!("UNKNOWN({v})"),
    }
}

/// `POST /api/purchase/abandon` — refund a stuck attempt and close it.
///
/// A separate path from `GET /api/purchase/{reference}` on purpose: this one
/// moves money BACK, and an operator should not be able to reach it by fumbling
/// a URL. Admin-floored like every mutation, and the reason is required.
pub async fn abandon_purchase(
    State(state): State<AppState>,
    Json(body): Json<AbandonPurchase>,
) -> Res<Value> {
    let out = state
        .monetize
        .purchase()
        .abandon(pb::AbandonPurchaseRequest {
            reference: body.reference,
            reason: body.reason,
            ..Default::default()
        })
        .await?
        .into_inner();
    let words = if out.released.is_empty() {
        format!("{} abandoned; it held nothing at a provider", out.reference)
    } else {
        format!(
            "{} abandoned; released {}",
            out.reference,
            out.released.join(", ")
        )
    };
    Ok(Json(
        json!({ "reference": out.reference, "released": out.released, "words": words }),
    ))
}

#[derive(Debug, serde::Deserialize)]
pub struct AbandonPurchase {
    pub reference: String,
    pub reason: String,
}

/// `POST /api/backup` — run `Ledger.Export` on monetize.
///
/// Behind a fresh touch (`needs_step_up`: every POST under `/api/` is), and it takes no
/// body: the destination is monetize's configured `backup_dir`, never a
/// caller's choice. See the proto — the export must run inside the service
/// because redb is single-process, so this route is the only way to take a
/// backup without stopping it.
pub async fn backup(State(state): State<AppState>) -> Res<Value> {
    let out = state
        .monetize
        .ledger()
        .export(pb::ExportBackupRequest {})
        .await?
        .into_inner();
    Ok(Json(json!({
        "dir": out.dir,
        "ledger_rows": out.ledger_rows,
        "usage_readings": out.usage_readings,
        "signing_public_key": out.signing_public_key,
        "contains_no_secrets": true,
    })))
}

/// `GET /api/ledger` — the open attempts first (the rows an operator must see),
/// then the last [`RECENT_ROWS`] rows of the ledger, newest first. The query
/// flag is accepted for the address's sake; both halves are always returned.
///
/// **"Recent" is now monetize's answer, not this layer's reconstruction.** It
/// used to be built here out of the ENTITLEMENT table: list every product's
/// facts, keep those whose `source` reads `payment:<vendor>:<reference>`, fetch
/// each of those references' rows, sort, truncate. A fact is not a row, so the
/// page was wrong in three ways at once — a DECLINED or RELEASED attempt never
/// makes a fact and so never appeared, though it is the one an operator is
/// looking for; an `operator` or `ladder` fact names no reference and so hid the
/// rows of the reference that had actually been bought for that tenant; and on a
/// server where nothing holds a fact yet it said "no rows yet" over a ledger with
/// rows in it. It was also N+2 round trips to monetize per page load, where the
/// ledger's own time index answers in one.
pub async fn ledger(
    State(state): State<AppState>,
    Query(q): Query<HashMap<String, String>>,
) -> Res<Value> {
    let open = state
        .monetize
        .ledger()
        .query(pb::QueryLedgerRequest {
            by: Some(By::Unfinished(true)),
        })
        .await?
        .into_inner();
    let unfinished: Vec<Value> = open
        .references
        .iter()
        .map(|r| json!({ "reference": r, "rows": open.rows.iter().filter(|x| &x.reference == r).map(row_json).collect::<Vec<_>>() }))
        .collect();
    let recent = state
        .monetize
        .ledger()
        .query(pb::QueryLedgerRequest {
            by: Some(By::Recent(RECENT_ROWS)),
        })
        .await?
        .into_inner();
    // The DEFAULT rendering `docs/v2/07-console.md` asks for: one line per attempt, the steps
    // behind a disclosure. `recent` stays because the disclosure needs it, but
    // it is no longer what the page leads with.
    let sales = sales_of(
        &state,
        q.get("product").map(String::as_str).unwrap_or(""),
        RECENT_SALES,
    )
    .await?;
    Ok(Json(
        json!({ "sales": sales, "unfinished": unfinished, "recent": recent.rows.iter().map(row_json).collect::<Vec<_>>() }),
    ))
}

/// How many ledger rows the Ledger page shows. monetize caps it lower if its own
/// ceiling is lower; asking for more than a page can be read is not a kindness.
pub const RECENT_ROWS: u32 = 200;

/// How many paid signups the Activity page lists. Smaller than [`RECENT_ROWS`]
/// because a signup is one line and an operator reads them, rather than
/// scrolling past them looking for a sale.
pub const RECENT_SIGNUPS: u32 = 100;

/// **`GET /api/signups`** — every paid signup the vendor webhooks brought in,
/// **refusals included**, newest first.
///
/// It is a separate call from `/api/ledger` and not a field on it, because a
/// signup is not a ledger attempt: it holds no reference, it has no tenant
/// until the sink answers, and half of them are refusals that never became a
/// sale at all. Joining them would make "what did this reference do" and "who
/// tried to sign up" one list ordered by two different clocks.
///
/// **The contact address IS here**, beside the account: this is the operator's
/// console behind the operator's login, and an account nobody can write to
/// cannot be run (measured 2026-09-15, the first paid signup). It is on
/// `SignupAdmin.List` only — never on `Signup.Status` or the event feed.
pub async fn signups(State(state): State<AppState>, Query(q): Query<SiteQuery>) -> Res<Value> {
    let site = site_vendor(&state, &q.estate).await?;
    let mut out = state
        .monetize
        .signups()
        .list(pb::ListSignupsRequest {
            limit: RECENT_SIGNUPS,
        })
        .await?
        .into_inner();
    out.signups.retain(|r| on_site(&r.vendor, &site));
    // Which rows monetize found by READING the vendor's own payments (the
    // reconcile pass) rather than from the vendor's message at checkout — so
    // the console can say where each row comes from. Best effort: a monetize
    // that cannot answer leaves every row "at checkout".
    let found: std::collections::BTreeSet<(String, String)> = match state
        .monetize
        .reconcile()
        .list_paid_at_vendor(pb::ListPaidAtVendorRequest { limit: 0 })
        .await
    {
        Ok(r) => r
            .into_inner()
            .cases
            .into_iter()
            .map(|c| (c.vendor, c.session))
            .collect(),
        Err(_) => Default::default(),
    };
    let rows: Vec<Value> = out
        .signups
        .iter()
        .map(|r| {
            let mut v = signup_json(r);
            v["found_at_vendor"] = json!(found.contains(&(r.vendor.clone(), r.session.clone())));
            v
        })
        .collect();
    Ok(Json(json!({ "signups": rows })))
}

/// How many paid-at-vendor cases the Activity page lists. A console page, not
/// an export; the counts beside it are the store's whole.
pub const RECENT_PAID_AT_VENDOR: u32 = 200;

/// **`GET /api/paid-at-vendor`** — the payments the VENDOR says it took that
/// monetize had no row for, newest first, with the counts the health line
/// carries.
///
/// The other half of the refusals panel. A refused callback is a delivery
/// monetize threw away; this is the money that was behind it, read back from
/// the vendor's own ledger by monetize's poller over the key it already holds
/// (`monetize_server::reconcile`). MEASURED 2026-09-14: an Apple Pay checkout
/// was paid at 11:10 UTC, every webhook for it was refused, and for hours the
/// only trace anywhere was a journald line. These rows are what that morning
/// would have shown.
///
/// Unlike the refusals, everything here is the VENDOR's word — monetize asked,
/// authenticated, and the vendor answered about its own object. A `settled`
/// row was handed to the same signup engine the webhook feeds (its outcome is
/// on the signups panel, same session id); a `needs-operator` row is one
/// monetize would not guess at, and `reason` says what was missing.
///
/// A read, like `/api/signups`: it carries no e-mail (monetize keeps it off
/// the wire), and "the customer was charged and monetize holds nothing" is
/// exactly what a viewer on duty must be able to see.
pub async fn paid_at_vendor(
    State(state): State<AppState>,
    Query(q): Query<SiteQuery>,
) -> Res<Value> {
    let site = site_vendor(&state, &q.estate).await?;
    let mut out = state
        .monetize
        .reconcile()
        .list_paid_at_vendor(pb::ListPaidAtVendorRequest {
            limit: RECENT_PAID_AT_VENDOR,
        })
        .await?
        .into_inner();
    if site.is_some() {
        // A site's own rows, and its own counts.
        out.cases.retain(|c| on_site(&c.vendor, &site));
        out.total = out.cases.len() as u64;
        out.needs_operator = out
            .cases
            .iter()
            .filter(|c| c.state == "needs-operator")
            .count() as u64;
        out.settled = out.cases.iter().filter(|c| c.state == "settled").count() as u64;
    }
    Ok(Json(json!({
        "cases": out.cases.iter().map(paid_at_vendor_json).collect::<Vec<_>>(),
        "total": out.total,
        "needs_operator": out.needs_operator,
        "settled": out.settled,
        "last_reconcile_unix_ms": out.last_reconcile_unix_ms.max(0),
    })))
}

/// One paid-at-vendor case for the console. Passed through field for field;
/// the currency is upper-cased because that is how every other amount on this
/// console spells its code, and the vendor spells it `sek`.
pub fn paid_at_vendor_json(c: &pb::PaidAtVendorCase) -> Value {
    json!({
        "vendor": c.vendor,
        "session": c.session,
        "reference": c.reference,
        "paid_at_unix_ms": c.paid_at_unix_ms.max(0),
        "amount_minor": c.amount_minor,
        "currency": c.currency.to_ascii_uppercase(),
        "tax_minor": c.tax_minor,
        "livemode": c.livemode,
        "metadata": c.metadata,
        "first_seen_unix_ms": c.first_seen_unix_ms.max(0),
        // `settled` | `needs-operator`, monetize's own word; an unrecognised
        // one is shown, never mapped to a friendlier one this console invented.
        "state": c.state,
        "reason": c.reason,
        "decided_at_unix_ms": c.decided_at_unix_ms.max(0),
    })
}

// ── The estate, drawn ─────────────────────────────────────────────────

/// **`GET /api/estate/{product}`** — the OLD product-addressed map: the ONE
/// monetize's board for `{product}`. LEGACY. It names no estate, because
/// the ONE monetize serves every estate; [`estate_map`] is the address.
pub async fn estate(
    State(state): State<AppState>,
    Path(product): Path<String>,
    headers: http::HeaderMap,
) -> Result<Response, ApiError> {
    draw_answer(&state, "", product, &headers, crate::bounded::PAGE_WITHIN).await
}

/// One drawn board, answered as [`svg_answer`] does, NAMING what it drew.
async fn draw_answer(
    state: &AppState,
    estate: &str,
    product: String,
    headers: &http::HeaderMap,
    within: std::time::Duration,
) -> Result<Response, ApiError> {
    // PROD-4: the map within the page's bound, else the last one drawn, aged.
    let mut ea = state.monetize.estate();
    let req = pb::DrawEstateRequest {
        product: product.clone(),
        estate: estate.to_owned(),
    };
    let (out, stale) = crate::bounded::within_for(
        &state.monetize.draws_seen,
        &format!("{product}/{estate}"),
        "the map",
        within,
        async move { ea.draw(req).await.map(|r| r.into_inner()) },
    )
    .await?;
    let stale = stale.map(|s| s.to_string()).unwrap_or_default();
    let mut answer = svg_answer(
        &out.svg,
        headers
            .get(http::header::IF_NONE_MATCH)
            .and_then(|v| v.to_str().ok()),
    );
    // No count from a monetize older than the field: no header, never a 0.
    let machines = out.machines.map(|n| n.to_string()).unwrap_or_default();
    for (k, v) in [
        ("x-monetize-estate", estate.to_owned()),
        ("x-monetize-product", product),
        ("x-monetize-machines", machines),
        ("x-monetize-stale-secs", stale),
    ] {
        if v.is_empty() {
            continue;
        }
        if let Ok(v) = http::HeaderValue::from_str(&v) {
            answer
                .headers_mut()
                .insert(http::HeaderName::from_static(k), v);
        }
    }
    Ok(answer)
}

/// **What is left of a page's [`crate::bounded::PAGE_WITHIN`]** since `t0` (PROD-4): a page that
/// asks several reads in turn answers within ONE bound; a read past it gets a moment to answer
/// from what it kept.
fn left_of(t0: std::time::Instant) -> std::time::Duration {
    crate::bounded::PAGE_WITHIN
        .saturating_sub(t0.elapsed())
        .max(std::time::Duration::from_millis(100))
}

/// The estate the fleet knows by `name`, refused by name when it knows none.
async fn fleet_estate(
    state: &AppState,
    name: &str,
) -> Result<crate::estates::FleetEstate, ApiError> {
    Ok(fleet_estate_aged(state, name).await?.0)
}

/// [`fleet_estate`], with the age of the fleet state it was read from (PROD-4).
async fn fleet_estate_aged(
    state: &AppState,
    name: &str,
) -> Result<(crate::estates::FleetEstate, Option<u64>), ApiError> {
    let (fleet, stale) = crate::estates::read_aged(&state.monetize).await?;
    fleet
        .into_iter()
        .find(|e| e.name == name)
        .map(|e| (e, stale))
        .ok_or_else(|| {
            ApiError(Fail {
                code: "notfound".into(),
                message: format!(
                    "the fleet knows no estate `{name}`: the ONE monetize has not declared it"
                ),
            })
        })
}

/// **`GET /api/estates/{estate}/map`** — that ESTATE's board, drawn by the
/// ONE monetize (`EstateAdmin.Draw`) for the product the fleet says the
/// estate serves. The estate is the path, and the answer NAMES what it drew
/// (`x-monetize-estate`, `x-monetize-product`, `x-monetize-machines`). An
/// estate the fleet does not know is 404. Nothing is asked of the estate.
///
/// Under `/api/`, behind `require_auth` like every read here;
/// four segments, so never the single-segment `*.svg` shape
/// `auth::is_public_asset` hands out. The caching decision is [`svg_answer`]'s.
pub async fn estate_map(
    State(state): State<AppState>,
    Path(estate): Path<String>,
    headers: http::HeaderMap,
) -> Result<Response, ApiError> {
    // PROD-4: the page's 2 s are the whole page's: the map gets what the fleet read left.
    let t0 = std::time::Instant::now();
    let one = fleet_estate(&state, &estate).await?;
    draw_answer(&state, &one.name, one.product, &headers, left_of(t0)).await
}

/// **`GET /api/iron`** — the ACCOUNT's iron, whole: per product, every estate's owned rows
/// (each naming its estate), the orphans and the missing (MAP-SEG 1). The operator's alone
/// (an operator's, [`crate::auth::require_auth`]); an estate's own pages carry their own rows only.
///
/// MAP-3 (owner 2026-10-27): ONE entry per cloud ACCOUNT ([`crate::iron::accounts`]), naming
/// every product that lists it, its rows grouped by estate with each estate's backups — the
/// page draws what it is handed.
pub async fn iron(State(state): State<AppState>) -> Res<Value> {
    let listed = list_products(&state).await?;
    let mut products = Vec::new();
    for p in &listed.products {
        let fleet = fleet_of(&state, &p.id).await?;
        products.push((p.id.clone(), fleet.cloud));
    }
    Ok(Json(
        json!({ "accounts": crate::iron::accounts(&products) }),
    ))
}

/// **`GET /api/overview`** — the estates that EXIST (OVW-CLEAN, owner 2026-10-29), as the ONE
/// monetize classifies them in ONE call (`Estates.Overview`): the rows (installed, or being
/// laid), the quiet line (ended or never installed), and the New-estate slot's choices. Each
/// row the fleet knows then asks for its own [`estate_summary`], so a slow part holds one row
/// and nothing else. Nothing is decided here.
pub async fn overview(State(state): State<AppState>) -> Result<Json<Value>, ApiError> {
    let (o, stale) = crate::estates::overview(&state.monetize).await?;
    Ok(Json(overview_json(&o, stale)))
}

/// Pure: **the Overview as the browser takes it**, field for field as monetize said it.
pub fn overview_json(o: &pb::EstatesOverviewResponse, stale_secs: Option<u64>) -> Value {
    let line = |e: &pb::EstateStanding| json!({ "name": e.name, "product": e.product, "standing": e.standing, "words": e.words, "in_fleet": e.in_fleet, "domain": e.domain });
    let slot = o.slot.clone().unwrap_or_default();
    json!({
        "estates": o.estates.iter().map(line).collect::<Vec<_>>(),
        "ended": o.ended.iter().map(line).collect::<Vec<_>>(),
        "slot": { "products": slot.products, "declared": slot.declared },
        "stale_secs": stale_secs,
    })
}

// ── THE ONE LEDGER'S ACCOUNTS (owner 2026-10-10) ───────────────────────────
//
// An account on the ONE ledger is named by its path: `gunnar/<t>` (the
// product's own estate), `gunnar/@<company>/<ns>` (a tenant of the sold estate
// <company>), `holger/<t>`. The Accounts page lists them (`/api/accounts`).

/// What the ONE ledger holds for an estate's summary, read in a few calls to
/// the ONE: every account, and the companies of the private instances sold.
struct Book {
    accounts: Vec<pb::AccountSummary>,
    instances: std::collections::BTreeSet<String>,
}

impl Book {
    /// Every estate the ledger shows as sold: a private instance's company,
    /// or an estate with `@<estate>/` tenants.
    fn sold(&self) -> std::collections::BTreeSet<String> {
        let mut s = self.instances.clone();
        s.extend(
            self.accounts
                .iter()
                .filter_map(|a| crate::estates::sold_estate_of(&a.tenant).map(str::to_owned)),
        );
        s
    }
}

async fn book(state: &AppState) -> Result<Book, ApiError> {
    let listed = state
        .monetize
        .accounts()
        .list(pb::ListAccountsRequest {
            product: String::new(),
            limit: 0,
        })
        .await?
        .into_inner();
    let mut instances = std::collections::BTreeSet::new();
    for a in listed.accounts.iter().filter(|a| a.instances > 0) {
        let rows = state
            .monetize
            .accounts()
            .rows(pb::AccountRowsRequest {
                product: a.product.clone(),
                tenant: a.tenant.clone(),
            })
            .await?
            .into_inner()
            .rows;
        instances.extend(
            rows.into_iter()
                .filter_map(|r| r.instance)
                .map(|i| i.company),
        );
    }
    Ok(Book {
        accounts: listed.accounts,
        instances,
    })
}

/// What counts as "this month" on the Overview: sales STARTED in the last 30
/// days.
pub const REVENUE_WINDOW_DAYS: u64 = 30;

/// **The month's money, split by each PAYMENT's own mode** — minor units per
/// currency (never summed across currencies, never a float).
#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
pub struct Takings {
    /// Real money: the payment said `livemode`.
    pub live: BTreeMap<String, u64>,
    /// Test money: the payment said so. Shown in brackets, never totalled.
    pub test: BTreeMap<String, u64>,
    /// The payment did not say (a vendor with no mode marker): neither.
    pub unsaid: BTreeMap<String, u64>,
    pub window_days: u64,
}

/// **Fold the money taken in the window**, by each sale's own `livemode`
/// (owner 2026-10-02: "derive this from each payment's Stripe livemode, never
/// from a label on the estate"). Money taken = a sale that DREW and was
/// GRANTED, started inside the window; its net price (VAT is not ours).
/// Declined, refunded and open attempts took nothing that stayed.
pub fn takings(sales: &[pb::Sale], now_ms: u64) -> Takings {
    let since = now_ms.saturating_sub(REVENUE_WINDOW_DAYS * 24 * 60 * 60 * 1000);
    let mut t = Takings {
        window_days: REVENUE_WINDOW_DAYS,
        ..Takings::default()
    };
    for s in sales
        .iter()
        .filter(|s| s.drawn && s.outcome == "granted" && s.started_unix_ms >= since)
    {
        let Some(price) = &s.price else { continue };
        if price.minor == 0 {
            continue;
        }
        let bucket = match s.livemode {
            Some(true) => &mut t.live,
            Some(false) => &mut t.test,
            None => &mut t.unsaid,
        };
        *bucket
            .entry(currency_name(price.currency).to_owned())
            .or_default() += price.minor;
    }
    t
}

/// **`GET /api/estates/{estate}/summary`** — ONE Overview row, read from the
/// ONE monetize and nothing else. It holds:
/// * the estate's product, as the fleet declared it;
/// * its machines, the boxes the fleet knows;
/// * `reached`, true when a box called home within the guardian's bound (else the pull model's words);
/// * its customers, the ONE ledger's accounts on it ([`crate::estates::tenant_on`]);
/// * the month's money from those accounts, split by each payment's own mode ([`takings`]);
/// * the vendor keys the ONE holds, and the money waiting at a vendor.
///
/// An estate the fleet does not know is 404. A ledger part that cannot be
/// read is `null`, which is not zero.
pub async fn estate_summary(
    State(state): State<AppState>,
    Path(estate): Path<String>,
) -> Result<Json<Value>, ApiError> {
    let t0 = std::time::Instant::now();
    let (one, stale) = fleet_estate_aged(&state, &estate).await?;
    let ledger = async {
        let b = book(&state).await.ok()?;
        let sold = b.sold().contains(&one.name);
        let mine = |product: &str, tenant: &str| {
            product == one.product && crate::estates::tenant_on(&one, sold, tenant)
        };
        let customers = b
            .accounts
            .iter()
            .filter(|a| mine(&a.product, &a.tenant))
            .count();
        let sales = state
            .monetize
            .ledger()
            .sales(pb::SalesRequest {
                product: one.product.clone(),
                limit: 0,
            })
            .await
            .ok()?
            .into_inner()
            .sales;
        let sales: Vec<pb::Sale> = sales
            .into_iter()
            .filter(|s| mine(&s.product, &s.tenant))
            .collect();
        Some((customers, takings(&sales, now_ms()), sold))
    };
    let listed = async { list_products(&state).await.ok() };
    // PROD-4: each part of a row is bounded; a part that does not answer is `null` on it.
    let within = left_of(t0);
    let (ledger, listed) = tokio::join!(
        async { tokio::time::timeout(within, ledger).await.ok().flatten() },
        async { tokio::time::timeout(within, listed).await.ok().flatten() }
    );
    // The open cases of THIS site's vendor only.
    let site = listed
        .as_ref()
        .map(|l| l.estate_vendors.get(&one.name).cloned().unwrap_or_default());
    let open = match site.as_ref() {
        Some(v) => tokio::time::timeout(within, site_open(&state, v))
            .await
            .ok()
            .flatten(),
        None => None,
    };
    Ok(Json(json!({
        "estate": one.name,
        "product": one.product,
        // PROD-4: the fleet did not answer within its bound; this row is its state this old.
        "stale_secs": stale,
        "reached": one.serving,
        "heard": one.heard,
        "last_heard_unix_ms": one.last_heard_unix_ms,
        "open_case": one.open_case,
        "error": one.error,
        "signed_with": state.monetize.signing_fingerprint(),
        "products": [one.product],
        // The machines that reported in, live (`estates::reported`), by role.
        "machines": one.reported.len(),
        "machine_roles": one.reported.clone(),
        "installed": one.installed,
        "machine_hosts": one.hosts,
        // A sold estate's machines are laid by this box; a site's own (gunnar.rs,
        // test2) by the operator's kit, with no label of this box on them.
        "laid_by_operator": ledger.as_ref().map(|(_, _, sold)| !*sold),
        "customers": ledger.as_ref().map(|(n, _, _)| *n),
        "revenue": ledger.as_ref().map(|(_, t, _)| t.clone()),
        "vendors": listed.as_ref().map(|l| l.vendor_modes.iter().map(|m| json!({ "vendor": m.vendor, "livemode": m.livemode })).collect::<Vec<_>>()).unwrap_or_default(),
        "vendor_names": listed.as_ref().map(|l| l.vendors.clone()).unwrap_or_default(),
        "clouds": listed.as_ref().map(|l| l.clouds.clone()).unwrap_or_default(),
        "default_cloud": listed.as_ref().map(|l| l.default_cloud.clone()).unwrap_or_default(),
        "paid_at_vendor_open": open,
        // The vendor this site bills through (`stripe/gunnar`), so the
        // Overview reads ITS keys.
        "site_vendor": site.unwrap_or_default(),
    })))
}

/// A strong `ETag` over the exact bytes served.
///
/// Honest because the renderer is deterministic: the same estate always yields
/// the same document, so equal hashes really do mean an unchanged picture —
/// there is no layout nondeterminism for the hash to launder.
pub fn svg_etag(svg: &str) -> String {
    format!("\"{}\"", blake3::hash(svg.as_bytes()).to_hex())
}

/// Does the caller's `If-None-Match` already hold this exact document? `*`
/// matches anything, and a weak validator (`W/"…"`) is accepted because weak
/// comparison is what a `GET` revalidation calls for.
pub fn etag_matches(if_none_match: Option<&str>, etag: &str) -> bool {
    let Some(raw) = if_none_match else {
        return false;
    };
    raw.split(',')
        .map(str::trim)
        .any(|t| t == "*" || t == etag || t.strip_prefix("W/") == Some(etag))
}

/// **The caching decision, in one place: revalidate always, transfer rarely.**
///
/// `Cache-Control: no-cache, private` — *store it, never use it without
/// asking*. A freshness lifetime is not available to this route however
/// tempting it looks: half the picture is LIVE (a box's disk, whether the twin
/// answered), so any seconds > 0 is a window in which an operator is shown an
/// estate that has changed, and this picture is opened precisely when something
/// has. `private` because the bytes are one operator's, and no shared cache
/// between the console and the browser may keep them.
///
/// The `ETag` then pays for itself on the transfer and not on the work: the
/// document has to be drawn before it can be hashed, so a `304` saves the
/// kilobytes and the browser's repaint, never the call into monetize. That is
/// the honest description of the saving, and it is still worth having on a
/// picture an operator leaves open and reloads.
fn svg_answer(body: &str, if_none_match: Option<&str>) -> Response {
    let etag = svg_etag(body);
    let headers = [
        (http::header::ETAG, etag.clone()),
        (http::header::CACHE_CONTROL, "no-cache, private".to_owned()),
    ];
    if etag_matches(if_none_match, &etag) {
        return (http::StatusCode::NOT_MODIFIED, headers).into_response();
    }
    (
        [(http::header::CONTENT_TYPE, "image/svg+xml".to_owned())],
        headers,
        body.to_owned(),
    )
        .into_response()
}

#[cfg(test)]
mod estate_tests {
    use super::*;

    /// ★ A DK price list is shown as DK in DKK — never silently as SE/SEK
    /// (owner 2026-09-28: holger sells in DK).
    #[test]
    fn a_danish_price_list_reads_dk_in_dkk() {
        assert_eq!(market_name(pb::Market::Dk as i32), "DK");
        assert_eq!(currency_name(pb::Currency::Dkk as i32), "DKK");
        assert_eq!(market_from_name("dk"), Some(pb::Market::Dk));
        assert_eq!(
            (
                market_name(pb::Market::Fi as i32),
                currency_name(pb::Currency::Eur as i32)
            ),
            ("FI", "EUR")
        );
    }

    #[test]
    fn an_etag_is_the_document_and_a_changed_estate_is_a_new_one() {
        let one = svg_etag("<svg/>");
        assert!(
            one.starts_with('"') && one.ends_with('"'),
            "a strong validator is quoted: {one}"
        );
        assert_eq!(
            one,
            svg_etag("<svg/>"),
            "the renderer is deterministic, so the tag must be"
        );
        assert_ne!(
            one,
            svg_etag("<svg />"),
            "one byte of difference is a different picture"
        );
    }

    #[test]
    fn if_none_match_holds_the_document_only_when_it_names_it() {
        let tag = svg_etag("<svg/>");
        assert!(etag_matches(Some(&tag), &tag));
        assert!(
            etag_matches(Some(&format!("W/{tag}")), &tag),
            "a weak validator revalidates a GET"
        );
        assert!(
            etag_matches(Some(&format!("\"other\", {tag}")), &tag),
            "a list is a list"
        );
        assert!(etag_matches(Some("*"), &tag));
        assert!(!etag_matches(None, &tag), "no header is never a match");
        assert!(
            !etag_matches(Some("\"other\""), &tag),
            "a stale tag must fetch the new picture"
        );
    }

    /// The trap this route was laid out to avoid: a single-segment `.svg` is
    /// handed out with NO session at all, because trunk fetches the console's
    /// own bundle without the cookie.
    #[test]
    fn the_estate_route_is_not_the_shape_that_is_public() {
        use http::Method;
        assert!(
            crate::auth::is_public_asset(&Method::GET, "/estate.svg"),
            "the trap is real"
        );
        assert!(
            !crate::auth::is_public_asset(&Method::GET, "/api/estate/gunnar"),
            "the estate must need a session"
        );
        assert!(!crate::auth::is_public("/api/estate/gunnar"));
    }

    /// The estate-addressed map is behind a session too — and its last
    /// segment is a WORD, so `.map` (a public asset extension) never matches.
    #[test]
    fn the_estate_addressed_map_is_not_a_public_asset() {
        use http::Method;
        for p in [
            "/api/estates/gunnar.rs/map",
            "/api/estates/test2.gunnar.rs/summary",
            "/api/overview",
        ] {
            assert!(!crate::auth::is_public_asset(&Method::GET, p), "{p}");
            assert!(!crate::auth::is_public(p), "{p}");
        }
    }

    /// ★ owner 2026-10-02: live money is live because the PAYMENT said so;
    /// test money is kept apart; a payment that said nothing is neither.
    #[test]
    fn takings_split_by_each_payments_own_mode_and_count_only_money_that_stayed() {
        let now = 1_800_000_000_000u64;
        let day = 24 * 60 * 60 * 1000;
        let s = |minor, cur: pb::Currency, live: Option<bool>, outcome: &str, drawn, started| {
            pb::Sale {
                price: Some(pb::Money {
                    minor,
                    currency: cur as i32,
                }),
                outcome: outcome.into(),
                drawn,
                started_unix_ms: started,
                livemode: live,
                ..Default::default()
            }
        };
        let t = takings(
            &[
                s(
                    100,
                    pb::Currency::Eur,
                    Some(true),
                    "granted",
                    true,
                    now - day,
                ),
                s(
                    50,
                    pb::Currency::Eur,
                    Some(true),
                    "granted",
                    true,
                    now - 29 * day,
                ),
                s(
                    80,
                    pb::Currency::Eur,
                    Some(false),
                    "granted",
                    true,
                    now - day,
                ),
                s(7, pb::Currency::Sek, None, "granted", true, now),
                s(
                    1_000,
                    pb::Currency::Eur,
                    Some(true),
                    "granted",
                    true,
                    now - 31 * day,
                ),
                s(2_000, pb::Currency::Eur, Some(true), "refunded", true, now),
                s(3_000, pb::Currency::Eur, Some(true), "declined", false, now),
                s(4_000, pb::Currency::Eur, Some(true), "open", false, now),
            ],
            now,
        );
        assert_eq!(t.live, BTreeMap::from([("EUR".to_string(), 150)]));
        assert_eq!(t.test, BTreeMap::from([("EUR".to_string(), 80)]));
        assert_eq!(t.unsaid, BTreeMap::from([("SEK".to_string(), 7)]));
        assert_eq!(t.window_days, 30);
    }

    /// ★ owner 2026-10-11: Stripe decides per country, each payment is a row —
    /// the row the console renders carries the buyer's country and the rate.
    #[test]
    fn a_sale_row_carries_the_buyers_country_and_the_vendors_rate() {
        let s = pb::Sale {
            customer_country: "DE".into(),
            vat_basis_points: 1_900,
            tax_from_vendor: true,
            tax: Some(pb::Money {
                minor: 190,
                currency: pb::Currency::Eur as i32,
            }),
            ..Default::default()
        };
        let j = sale_json(&s);
        assert_eq!(
            (j.customer_country.as_str(), j.vat_basis_points, j.tax_minor),
            ("DE", 1_900, Some(190))
        );
    }
}

/// **The number the health line carries**: paid-at-vendor rows a person still
/// has to act on. `None` when monetize could not be asked for it — an older
/// monetize without the service, or a read that failed — which is a different
/// thing from zero, and the badge must not say "nothing open" on the strength
/// of a call that did not happen.
/// The open paid-at-vendor cases of one vendor instance.
async fn site_open(state: &AppState, vendor: &str) -> Option<u64> {
    let out = state
        .monetize
        .reconcile()
        .list_paid_at_vendor(pb::ListPaidAtVendorRequest { limit: 0 })
        .await
        .ok()?
        .into_inner();
    Some(
        out.cases
            .iter()
            .filter(|c| c.vendor == vendor && c.state == "needs-operator")
            .count() as u64,
    )
}

async fn paid_at_vendor_open(state: &AppState) -> Option<u64> {
    let out = state
        .monetize
        .reconcile()
        .list_paid_at_vendor(pb::ListPaidAtVendorRequest { limit: 1 })
        .await
        .ok()?
        .into_inner();
    Some(out.needs_operator)
}

/// How many refused callbacks the Activity page lists. The whole ring: monetize
/// keeps 128 and there is nothing behind it to page into, so asking for fewer
/// would hide rows with no way left to ask for them.
pub const RECENT_REFUSALS: u32 = 128;

/// **`GET /api/callback-refusals`** — the vendor deliveries monetize REFUSED,
/// newest first, with the counter that says how many more there were.
///
/// The failure this exists for is the one that used to be invisible: a customer
/// is charged at the vendor, the webhook arrives, monetize refuses it — a wrong
/// clock, a rotated secret, a payload it stopped understanding — and NOTHING is
/// written. There is no ledger row (correctly: the callback authorised
/// nothing), no signup record, no entitlement move. Measured 2026-09-14 on the
/// front, where a two-hour clock skew put every signed Stripe timestamp outside
/// the 300 s tolerance and the only trace was journald.
///
/// **Everything under `refusals` is UNAUTHENTICATED INPUT** unless its
/// `verified` is true — that path takes no bearer, which is the whole reason a
/// callback can be refused at all. The console must render `detail` as text and
/// must say beside it whose words they are. Nothing here is an instruction, a
/// reference, or a name anything else looks up.
///
/// A read, like `/api/signups` beside it: "money moved at the vendor and
/// monetize holds nothing" is precisely what an operator on duty must be able
/// to see, and a viewer is on duty too. It carries no customer, no amount and
/// no address to leak — monetize refused the delivery before it had any.
pub async fn callback_refusals(State(state): State<AppState>) -> Res<Value> {
    let out = state
        .monetize
        .callbacks()
        .list_refusals(pb::ListRefusalsRequest {
            limit: RECENT_REFUSALS,
        })
        .await?
        .into_inner();
    Ok(Json(json!({
        "refusals": out.refusals.iter().map(refusal_json).collect::<Vec<_>>(),
        // Both numbers, always: `total_since_start` above `kept` is the only way
        // an operator learns that older refusals fell out of the ring, and a
        // list that showed the rows without the count would read as complete.
        "total_since_start": out.total_since_start,
        "kept": out.kept,
        "capacity": out.capacity,
        "first_at_unix_ms": out.first_at_unix_ms,
        "last_at_unix_ms": out.last_at_unix_ms,
    })))
}

/// One refused callback for the console. Passed through field for field — this
/// layer joins nothing onto it and looks nothing up from it, which is the whole
/// posture towards a row a stranger can cause.
pub fn refusal_json(r: &pb::CallbackRefused) -> Value {
    json!({
        "at_unix_ms": r.at_unix_ms,
        // `unverified` | `not-understood` | `handle-mismatch`. monetize's own
        // word for its own arm; an unrecognised one is shown, never mapped to a
        // friendlier one this console invented.
        "refusal": r.kind,
        "vendor": r.vendor,
        // **Whose words `detail` holds.** False is a caller who proved nothing.
        "verified": r.verified,
        "detail": r.detail,
        "body_bytes": r.body_bytes,
        "refusal_seq": r.refusal_seq,
    })
}

/// One signup row for the console. `state` is the wire enum as a lowercase word
/// (`pending` | `created` | `refused` | `unknown`), the way every other enum
/// reaches this console — the browser must not carry a number whose meaning
/// lives in a `.proto` it never sees.
pub fn signup_json(r: &pb::SignupRow) -> Value {
    json!({
        "vendor": r.vendor,
        "session": r.session,
        "state": signup_state_name(r.state),
        "username": r.username,
        "email": r.email,
        "namespace": r.namespace,
        "reason": r.reason,
        "product": r.product,
        "plan": r.plan,
        "amount_minor": r.amount.as_ref().map(|m| m.minor).unwrap_or(0),
        "currency": r.amount.as_ref().map(|m| currency_name(m.currency)).unwrap_or("SEK"),
        // The vendor account's mode, read off the payment's own id (`cs_live_…`).
        "livemode": r.livemode,
        "paid_at_unix_ms": r.paid_at_unix_ms,
        "recorded_at_unix_ms": r.recorded_at_unix_ms,
        "decided_at_unix_ms": r.decided_at_unix_ms,
        // **Money taken and no account**: what is owed and what a human must do
        // about it, as monetize wrote it. The sentence travels rather than being
        // rebuilt here — one payment must not have two sets of instructions.
        "owed": signup_owed_name(r.owed),
        "remedy": r.remedy,
    })
}

/// An unrecognised `owed` is `unknown`, never `none`: a row this console does
/// not recognise may still be somebody's money, and the safe wrong answer is
/// the one that makes an operator look.
fn signup_owed_name(v: i32) -> &'static str {
    use pb::signup_row::Owed as O;
    match O::try_from(v) {
        Ok(O::Unspecified) => "none",
        Ok(O::Unrecorded) => "unrecorded",
        Ok(O::Refund) => "refund",
        Ok(O::RefundAndTheNameMayBeSpent) => "refund_and_the_name_may_be_spent",
        Ok(O::RenameOrRefund) => "rename_or_refund",
        Err(_) => "unknown",
    }
}

/// An unrecognised state is `unknown`, never defaulted to `pending`: "we are
/// still working on it" is a promise, and this layer must not make one on
/// behalf of a monetize whose enum it does not recognise.
fn signup_state_name(v: i32) -> &'static str {
    use pb::signup_status_response::State as S;
    match S::try_from(v) {
        Ok(S::Pending) => "pending",
        Ok(S::Created) => "created",
        Ok(S::Refused) => "refused",
        Ok(S::Unknown) | Ok(S::Unspecified) | Err(_) => "unknown",
    }
}

#[derive(Debug, Deserialize)]
pub struct SetBody {
    /// A free label the operator may leave on a hand-set fact (it rides in
    /// the fact's `plan` field, which a purchase fills with the order
    /// reference). Empty is the normal answer: "hand-set".
    #[serde(default)]
    pub label: String,
    pub state: String,
    #[serde(default)]
    pub paid_until_unix_ms: Option<u64>,
    /// **Absent means KEEP, not clear** — see [`set_entitlement`], where the
    /// three cases are told apart. `Option` and not `#[serde(default)]`
    /// precisely so that "the form did not mention caps" and "the caller asked
    /// for none" stop being the same value.
    #[serde(default)]
    pub caps: Option<BTreeMap<String, u64>>,
}

/// `POST /api/entitlement/{product}/{tenant}` — the operator's `Entitlement.Set`:
/// source `operator`, empty signature ("sign it for me"). Admin only, by `role_floor`.
///
/// # The caps a state change must not eat
///
/// `Entitlement.Set` REPLACES a fact; it does not merge into one. The console's
/// "Set entitlement…" form posts a label, a state and a date and says nothing
/// about caps, because caps are bought rather than typed — so while
/// [`SetBody::caps`] was a plain map with `#[serde(default)]`, an absent field
/// arrived as an EMPTY map and moving a paying tenant to `grace` for a week
/// deleted their `pack_bytes` and `cache_bytes`. On gunnar an absent cap means
/// **unlimited** (`gunnar_core::Entitlement::pack_cap`, "0 is unlimited"), so
/// the punishment for not paying was an uncapped plan and an unbounded disk.
///
/// Three cases, kept apart here because this is the last place they still can
/// be — the proto's `caps` is a plain map and cannot express the difference:
///
/// | body | meaning |
/// |---|---|
/// | no `caps` key | **keep** what the tenant already holds |
/// | `"caps": {…}` | **set** exactly that, replacing what is stored |
/// | `"caps": {}` | **clear** — deliberate, typed, and still reachable |
///
/// The carry-forward costs one `Entitlement.Get` on a route an admin hits by
/// hand. A tenant with no fact yet carries nothing forward and is not an error:
/// a first fact must not need a previous one.
pub async fn set_entitlement(
    State(state): State<AppState>,
    Path((product, tenant)): Path<(String, String)>,
    Json(body): Json<SetBody>,
) -> Res<Value> {
    let st = state_from_name(&body.state).ok_or_else(|| {
        ApiError(Fail::invalid(format!(
            "state {:?} is not free, paid, grace, suspended or retention",
            body.state
        )))
    })?;
    let caps = match body.caps {
        Some(caps) => caps,
        None => state
            .monetize
            .entitlement()
            .get(pb::GetEntitlementRequest {
                product: product.clone(),
                tenant: tenant.clone(),
            })
            .await?
            .into_inner()
            .fact
            .map(|f| f.caps)
            .unwrap_or_default(),
    };
    let fact = pb::EntitlementFact {
        tenant: tenant.clone(),
        plan: body.label,
        state: st as i32,
        paid_until_unix_ms: body.paid_until_unix_ms,
        caps,
        source: "operator".into(),
        signature: Vec::new(),
        issued_unix_ms: None,
        issued_signature: Vec::new(),
    };
    let r = state
        .monetize
        .entitlement()
        .set(pb::SetEntitlementRequest {
            product: product.clone(),
            fact: Some(fact),
        })
        .await?
        .into_inner();
    tracing::info!(target: crate::auth::AUDIT_TARGET, event = "entitlement.set", product = %product, tenant = %tenant, pushed = r.pushed, "an operator set an entitlement");
    state.events.poke();
    let verifier = verifier(&state, &product).await?;
    Ok(Json(
        json!({ "fact": r.fact.as_ref().map(|f| fact_json(f, &verifier)), "pushed": r.pushed, "push_error": r.push_error }),
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn fact(tenant: &str, state: pb::State) -> pb::EntitlementFact {
        pb::EntitlementFact {
            tenant: tenant.into(),
            plan: "10gb-se".into(),
            state: state as i32,
            paid_until_unix_ms: Some(5),
            caps: BTreeMap::new(),
            source: "payment:invoice/x".into(),
            signature: vec![1],
            issued_unix_ms: None,
            issued_signature: Vec::new(),
        }
    }

    #[test]
    fn the_join_is_by_tenant_and_a_fact_less_tenant_is_free() {
        let usages = [pb::UsageReport {
            tenant: "a".into(),
            meters: vec![pb::Meter {
                name: "pack_bytes".into(),
                value: 7,
            }],
            measured_at_unix_ms: 9,
        }];
        let facts = [fact("b", pb::State::Grace), fact("a", pb::State::Paid)];
        let rows = join_tenants(&usages, &facts, &Verifier::none());
        assert_eq!(rows.len(), 2);
        assert_eq!(
            (
                rows[0].tenant.as_str(),
                rows[0].state,
                rows[0].meters["pack_bytes"],
                rows[0].measured_at_unix_ms
            ),
            ("a", "paid", 7, Some(9))
        );
        assert_eq!(
            (
                rows[1].tenant.as_str(),
                rows[1].state,
                rows[1].measured_at_unix_ms
            ),
            ("b", "grace", None)
        );
        let only_usage = join_tenants(&usages, &[], &Verifier::none());
        assert_eq!(
            (
                only_usage[0].state,
                only_usage[0].order.as_str(),
                only_usage[0].signed
            ),
            ("free", "", false)
        );
    }

    #[test]
    fn a_cap_on_an_undeclared_meter_is_refused_by_name() {
        let declared = vec![MeterInfo {
            name: "pack_bytes".into(),
            meaning: String::new(),
            enforcement: "enforced".into(),
            scope: "tenant".into(),
            unit: "disk_gib_month".into(),
        }];
        let mut caps = BTreeMap::new();
        caps.insert("pack_bytes".to_string(), 1);
        assert!(validate_caps("gunnar", &declared, &caps).is_ok());
        caps.insert("lfs_bytes".to_string(), 1);
        let e = validate_caps("gunnar", &declared, &caps).unwrap_err();
        assert!(e.contains("\"lfs_bytes\"") && e.contains("gunnar"), "{e}");
    }

    #[test]
    fn the_cloud_block_passes_its_currency_through_and_an_older_server_sends_none() {
        let cloud = pb::CloudInventory {
            provider: "sandbox".into(),
            available: true,
            monthly_minor: 230,
            currency: "SEK".into(),
            ..Default::default()
        };
        let f = pb::FleetResponse {
            cloud: Some(cloud),
            ..Default::default()
        };
        let j = fleet_json(&f).cloud;
        assert_eq!(
            (j["monthly_minor"].as_u64(), j["currency"].as_str()),
            (Some(230), Some("SEK")),
            "{j}"
        );
        let none = fleet_json(&pb::FleetResponse::default()).cloud;
        assert_eq!(
            none["currency"].as_str(),
            Some(""),
            "no block is no currency, not a guessed one: {none}"
        );
    }

    /// **A quote arrives with every money figure, its currency, the delta and
    /// the iron in words** — and a query-string order parses, or refuses by
    /// naming the entry.
    #[test]
    fn a_quote_is_shaped_with_its_money_delta_and_iron_and_a_caps_query_parses() {
        let q = pb::QuoteResponse {
            reference: "gunnar/alice/2026-09-05+pack_bytes=107374182400".into(),
            price: Some(pb::Money {
                minor: 198_000,
                currency: pb::Currency::Sek as i32,
            }),
            cost: Some(pb::Money {
                minor: 2_070,
                currency: pb::Currency::Eur as i32,
            }),
            current: [("pack_bytes".to_string(), 10u64 << 30)].into(),
            caps: [("pack_bytes".to_string(), 100u64 << 30)].into(),
            delta: [("pack_bytes".to_string(), 90u64 << 30)].into(),
            resources: vec![pb::Resource {
                kind: Some(pb::resource::Kind::Disk(pb::resource::Disk {
                    server: "gunnar-se-sto1".into(),
                    gib: 90,
                })),
            }],
            can_serve: false,
            reason: "the cloud cannot serve this for alice: short 5 GiB".into(),
            period: "month".into(),
            words: String::new(),
            // The operator console formats its own amount from `price` — this
            // test asserts exactly that — so the customer-facing string plays
            // no part here.
            price_words: String::new(),
        };
        let j = quote_json(&q);
        assert_eq!(
            (j.price_minor, j.currency, j.cost_minor, j.cost_currency),
            (198_000, "SEK", 2_070, "EUR")
        );
        assert_eq!(j.delta["pack_bytes"], 90u64 << 30);
        assert_eq!(
            (j.resources[0].kind.as_str(), j.resources[0].detail.as_str()),
            ("disk", "90 GiB on gunnar-se-sto1")
        );
        assert!(!j.can_serve && j.reason.contains("short 5 GiB"));
        assert_eq!(
            parse_caps_query("pack_bytes=107374182400, cache_bytes=5").unwrap(),
            [
                ("pack_bytes".to_string(), 107_374_182_400u64),
                ("cache_bytes".to_string(), 5)
            ]
            .into()
        );
        assert!(parse_caps_query("").unwrap().is_empty());
        assert!(parse_caps_query("pack_bytes")
            .unwrap_err()
            .contains("meter=value"));
        assert!(parse_caps_query("pack_bytes=lots")
            .unwrap_err()
            .contains("\"lots\""));
        assert_eq!(state_from_name("paid").map(state_name), Some("paid"));
        let list = pb::PriceList {
            product: "gunnar".into(),
            market: pb::Market::Fi as i32,
            currency: pb::Currency::Eur as i32,
            units: [("disk_gib_month".to_string(), 200u64)].into(),
        };
        assert_eq!(
            price_list_json(&list),
            PriceListJson {
                product: "gunnar".into(),
                market: "FI".into(),
                currency: "EUR".into(),
                units: [("disk_gib_month".to_string(), 200u64)].into()
            }
        );
        assert_eq!(market_from_name("se"), Some(pb::Market::Se));
        assert_eq!(
            policy_json(None).period,
            "",
            "an older monetize names no period; none is guessed"
        );
    }
}

// ── The TAX page ──────────────────────────────────────────────────────

/// **`GET /api/tax/periods?year=2026`** — the periods that year is filed in,
/// and the turnover that chose them.
///
/// A separate call from the report because it is a different question and a
/// different answer size: the page asks this once when a year is picked and
/// asks for a report each time a period is clicked. Folding them into one would
/// mean recomputing the whole ledger's turnover on every click of a period the
/// page already has.
pub async fn tax_periods(
    State(state): State<AppState>,
    Query(q): Query<HashMap<String, String>>,
) -> Res<Value> {
    let year = tax_year(&q)?;
    let out = state
        .monetize
        .tax()
        .periods(pb::TaxPeriodsRequest { year })
        .await?
        .into_inner();
    Ok(Json(json!({
        "jurisdiction": out.jurisdiction,
        "turnover_minor": out.turnover.as_ref().map(|m| m.minor).unwrap_or(0),
        "currency": out.turnover.as_ref().map(|m| currency_name(m.currency)).unwrap_or("SEK"),
        "turnover_year": out.turnover_year,
        "periods": out.periods.iter().map(tax_period_json).collect::<Vec<_>>(),
    })))
}

/// **`GET /api/tax/report?year=2026&period=2026-Q1`** — the filled form.
///
/// The CSV rides along on the JSON rather than being a second call: it is the
/// same fold, monetize already rendered it, and a download that re-asked the
/// server could return a file that disagrees with the table the operator is
/// looking at.
pub async fn tax_report(
    State(state): State<AppState>,
    Query(q): Query<HashMap<String, String>>,
) -> Res<Value> {
    let year = tax_year(&q)?;
    let period = q.get("period").cloned().unwrap_or_default();
    if period.trim().is_empty() {
        return Err(ApiError(Fail::invalid(
            "period is required; take one from /api/tax/periods",
        )));
    }
    let out = state
        .monetize
        .tax()
        .report(pb::TaxReportRequest { year, period })
        .await?
        .into_inner();
    Ok(Json(json!({
        "jurisdiction": out.jurisdiction,
        "period": out.period.as_ref().map(tax_period_json).unwrap_or(Value::Null),
        // **Absent stays absent.** A box nobody computed arrives with no
        // `amount_minor` at all, and the page prints `—`. Defaulting it to 0
        // here would invent a figure at the last hop.
        "lines": out.lines.iter().map(|l| json!({
            "box_id": l.box_id,
            "label": l.label,
            "amount_minor": l.amount_minor,
            "currency": l.currency,
        })).collect::<Vec<_>>(),
        "notes": out.notes,
        "csv": out.csv,
    })))
}

/// The year on a tax query. Refused here rather than passed on when it is not a
/// year at all, so the message names the parameter the browser sent.
fn tax_year(q: &HashMap<String, String>) -> Result<u32, ApiError> {
    let raw = q.get("year").map(String::as_str).unwrap_or("").trim();
    if raw.is_empty() {
        return Err(ApiError(Fail::invalid("year is required, e.g. year=2026")));
    }
    raw.parse::<u32>()
        .map_err(|_| ApiError(Fail::invalid(format!("year={raw:?} is not a year"))))
}

fn tax_period_json(p: &pb::TaxPeriod) -> Value {
    json!({ "label": p.label, "kind": p.kind, "from_unix_ms": p.from_unix_ms, "to_unix_ms": p.to_unix_ms })
}

#[cfg(test)]
mod tax_tests {
    use super::*;

    #[test]
    fn a_year_query_is_required_and_must_be_a_year() {
        let none: HashMap<String, String> = HashMap::new();
        assert!(tax_year(&none)
            .expect_err("no year")
            .0
            .message
            .contains("year is required"));
        let bad: HashMap<String, String> = [("year".to_string(), "last".to_string())].into();
        assert!(tax_year(&bad)
            .expect_err("not a year")
            .0
            .message
            .contains("\"last\""));
        let good: HashMap<String, String> = [("year".to_string(), " 2026 ".to_string())].into();
        assert_eq!(tax_year(&good).ok(), Some(2026));
    }

    #[test]
    fn a_period_travels_with_its_kind_and_its_window() {
        let p = pb::TaxPeriod {
            label: "2026-Q2".into(),
            kind: "quarterly".into(),
            from_unix_ms: 10,
            to_unix_ms: 20,
        };
        assert_eq!(
            tax_period_json(&p),
            json!({ "label": "2026-Q2", "kind": "quarterly", "from_unix_ms": 10, "to_unix_ms": 20 })
        );
    }
}