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/tests/screens_endpoints.rs

//! **Every screen endpoint against the mock, field by field, from the fixture.**
//!
//! `UI.md`'s screens read their routes. Each is asserted here on EXACT
//! values out of `uitest/src/fixture.rs` — the same numbers the browser matrix
//! looks for in the DOM — and on the conventions the shapes promise:
//! `per_week` and `capacity` are `null` when unknown, never `0`;
//! `may_project == false` forbids a date; `oversell == "unmeasured"` is not a
//! `fits`; `is_a_floor` marks a floor. A fixture with distinct, non-zero
//! numbers is what makes these assertions worth anything: a zero cannot tell a
//! wired field from a dead one.

#[path = "../../../uitest/src/fixture.rs"]
mod fixture;
#[path = "../../../uitest/src/mock.rs"]
mod mock;
mod screens_rig;

use fixture as fx;
use http::{Method, StatusCode};
use monetize_ui_server::role::Role;
use screens_rig::{n, Auth, Rig};
use serde_json::Value;

fn meter<'a>(fleet: &'a Value, name: &str) -> &'a Value {
    fleet["meters"].as_array().unwrap().iter().find(|m| m["meter"] == name).unwrap_or_else(|| panic!("no fleet meter {name} in {fleet}"))
}

/// (r) **Screen 1: one card per product with its dot, its figure and its rate.**
///
/// RED first: with `per_week` asserted as `0.0` for idun this fails — it is
/// `null`, and that is the point.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn screen1_products_carries_every_row_with_its_dot_figure_and_rate() {
    let rig = Rig::up().await;
    let body = rig.get("/api/products").await;
    let cards = body["products"].as_array().expect("cards");
    let ids: Vec<&str> = cards.iter().map(|c| c["id"].as_str().unwrap()).collect();
    assert_eq!(ids, fx::PRODUCT_IDS, "one row per product, in monetize's order");
    assert_eq!(body["vendors"], serde_json::json!(fx::VENDORS));

    let (gunnar, holger, idun, skade) = (&cards[0], &cards[1], &cards[2], &cards[3]);

    // THE DOT. Three states, and the plugin's words for `down`.
    assert_eq!(gunnar["reachable"]["state"], "up");
    assert_eq!(holger["reachable"]["state"], "down");
    assert_eq!(holger["reachable"]["detail"], fx::HOLGER_DOWN_DETAIL, "`not connected` needs its reason");
    assert_eq!(holger["reachable"]["last_up_unix_ms"], n(0), "never answered");
    assert_eq!(holger["reachable"]["checked_at_unix_ms"], n(fx::T0), "a cached answer says how old it is");

    // THE TENANT COUNT, per product.
    assert_eq!((gunnar["tenants"].as_u64(), holger["tenants"].as_u64(), idun["tenants"].as_u64(), skade["tenants"].as_u64()), (Some(7), Some(0), Some(1), Some(2)));
    assert_eq!(gunnar["by_state"], serde_json::json!({ "paid": 5, "grace": 1, "free": 1 }));

    // THE RATE — one number, with its uncertainty, or NOTHING.
    let t = &gunnar["trend"];
    assert_eq!((t["known"].as_bool(), t["meter"].as_str()), (Some(true), Some("pack_bytes")), "{t}");
    assert_eq!(t["per_week"].as_f64(), Some(fx::GUNNAR_FLEET_PER_WEEK));
    assert_eq!((t["confidence"].as_str(), t["marker"].as_str(), t["may_project"].as_bool()), (Some("approximate"), Some("~"), Some(true)), "an approximate rate projects, with its tilde");
    assert_eq!(t["window_ms"], n(fx::WINDOW_30D_MS), "`last 30 d` on the screen");
    // idun: NOT ENOUGH HISTORY. `per_week` is null — and asserting it is
    // `0.0` instead would be the bug this test exists to refuse.
    let t = &idun["trend"];
    assert_eq!(t["known"], false);
    assert!(t["per_week"].is_null(), "unknown is null, never zero: {t}");
    assert_ne!(t["per_week"], n(0));
    assert_eq!(t["needed_span_ms"], n(fx::NEEDED_SPAN_MS));
    assert!(holger["trend"]["per_week"].is_null(), "{}", holger["trend"]);
    // skade: known and BURSTY — the rate stands, the date is forbidden.
    let t = &skade["trend"];
    assert_eq!((t["known"].as_bool(), t["per_week"].as_f64(), t["confidence"].as_str()), (Some(true), Some(fx::SKADE_FLEET_PER_WEEK), Some("bursty")));
    assert_eq!((t["may_project"].as_bool(), t["marker"].as_str()), (Some(false), Some("~")), "one push must not become a date: {t}");

    // THE FIGURE: `used of capacity`, or capacity NULL — never 0.
    let pack = meter(&gunnar["fleet"], "pack_bytes");
    assert_eq!(pack["used"], n(fx::gunnar_pack_used()), "the sum of the seven readings");
    assert_eq!(pack["capacity"], n(fx::GUNNAR_CAPACITY.raw));
    assert_eq!(pack["allowance"], n(fx::GUNNAR_ALLOWANCE));
    assert_eq!((pack["oversell"].as_str(), pack["oversell_bytes"].as_u64()), (Some("fits"), Some(fx::GUNNAR_HEADROOM)));
    assert_eq!((pack["tenants_without_cap"].as_u64(), pack["is_a_floor"].as_bool()), (Some(1), Some(true)), "bjorn names no cap, so the allowance is a floor");
    let archive = meter(&idun["fleet"], "archive_bytes");
    assert!(archive["capacity"].is_null(), "an unmeasured box has no denominator: {archive}");
    assert_ne!(archive["capacity"], n(0));
    assert_eq!(archive["oversell"], "unmeasured", "silence is not a verdict");
    assert_eq!(archive["oversell_reason"], fx::IDUN_UNMEASURED_REASON);
    assert_eq!(archive["used"], n(fx::OLA_ARCHIVE.raw), "the half monetize CAN fold is still there");
    assert_eq!(idun["fleet"]["servable_measured"], false);
    assert_eq!(idun["fleet"]["servable_at_unix_ms"], n(0), "never asked");
    let store = meter(&skade["fleet"], "store_bytes");
    assert_eq!((store["oversell"].as_str(), store["oversell_bytes"].as_u64()), (Some("oversold"), Some(fx::SKADE_OVERSOLD.raw)), "two tenants promised 16 GiB on a 12 GiB box");
    assert_eq!((store["capacity"].as_u64(), store["is_a_floor"].as_bool()), (Some(fx::SKADE_CAPACITY.raw), Some(false)));
    assert_eq!(store["used"], n(fx::skade_store_used()));

    // GROWTH — the badge's word and the paragraph behind it, verbatim.
    assert_eq!(gunnar["fleet"]["growth"]["state"], "sealed");
    assert_eq!(gunnar["fleet"]["growth"]["detail"], fx::SEALED_DETAIL);
    assert_eq!(skade["fleet"]["growth"]["state"], "operator_only");
    assert_eq!(idun["fleet"]["growth"]["state"], "", "a box nobody could measure has not said whether it can grow — and that is not `unknown`");

    // The console is told which meters are per-tenant and which are the product's.
    let scopes: Vec<(&str, &str)> = gunnar["meters"].as_array().unwrap().iter().map(|m| (m["name"].as_str().unwrap(), m["scope"].as_str().unwrap())).collect();
    assert!(scopes.contains(&("pack_bytes", "tenant")) && scopes.contains(&("cpu_millicores", "product")), "{scopes:?}");
    assert!(!gunnar["fleet"]["meters"].as_array().unwrap().iter().any(|m| m["meter"] == "cpu_millicores"), "a product-scoped meter is in NO fleet sum");

    // Both product-shaped calls went to monetize for each product, by name.
    // RA3-1: asked at once, so in no fixed order — each product once.
    let mut fleets = rig.mock.calls.fleets_asked.lock().unwrap().clone();
    fleets.sort();
    let mut want: Vec<String> = fx::PRODUCT_IDS.iter().map(|s| s.to_string()).collect();
    want.sort();
    assert_eq!(fleets, want);
}

/// (r) **Screen 1's spend line: `Lifetime · 2.26 EUR spent · 10.00 EUR cap`.**
///
/// The ledger's fold against the cloud's LIFETIME ceiling, top level on this
/// route. The monthly ceiling is unset in the fixture and must arrive as
/// `null` — zero would mean "refuse every purchase".
///
/// RED first (2026-09-05, before `34f468e`): `/api/products` had keys
/// `["clouds", "products", "vendors"]` and nothing carried the figure.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn screen1_the_spend_line_has_its_figures_on_the_wire() {
    let rig = Rig::up().await;
    let body = rig.get("/api/products").await;
    let spend = &body["spend"][0];
    assert!(spend.is_object(), "`/api/products` carries no spend line; the keys are {:?}", body.as_object().unwrap().keys().collect::<Vec<_>>());
    assert_eq!(body["default_cloud"], "upcloud", "the one cloud linked is where a sale provisions");
    assert_eq!((spend["provider"].as_str(), spend["currency"].as_str()), (Some("upcloud"), Some("EUR")), "{spend}");
    assert_eq!(spend["spent_minor"], n(fx::SPEND_SPENT_MINOR), "the ledger's fold, in minor units");
    assert_eq!(spend["ceiling_total_minor"], n(fx::SPEND_CEILING_MINOR), "the lifetime cap");
    assert!(spend["ceiling_monthly_minor"].is_null(), "an unset limit is null, never 0: {spend}");
    assert_ne!(spend["ceiling_monthly_minor"], n(0));

    // CLOUD: what we own at the provider, on the fleet, reconciled.
    // MAP-SEG 1: a product CARD carries the account's totals and no row; the rows are the
    // operator's account-wide Iron page's (`/api/iron`) and each estate's own Map's.
    let card = &body["products"][0]["fleet"]["cloud"];
    assert_eq!((card["available"].as_bool(), card["provider"].as_str(), card["currency"].as_str(), card["total_bytes"].as_u64()), (Some(true), Some("upcloud"), Some("EUR"), Some(20 * fx::GIB)), "{card}");
    assert!(card["owned"].as_array().unwrap().is_empty(), "a card hands out no row: {card}");
    let viewer = rig.cookie(Role::Viewer);
    let (refused, _, _) = rig.call(Method::GET, "/api/iron", Auth::Cookie(&viewer), None).await;
    assert_eq!(refused, StatusCode::FORBIDDEN, "the account's iron is the operator's, not a viewer's");
    let admin = rig.cookie(Role::Admin);
    let (ok, iron, _) = rig.call(Method::GET, "/api/iron", Auth::Cookie(&admin), None).await;
    assert_eq!(ok, StatusCode::OK, "{iron}");
    // MAP-3: one entry per cloud account, naming the products that list it.
    let cloud = &iron["accounts"][0];
    assert_eq!(iron["accounts"].as_array().map(Vec::len), Some(1), "{iron}");
    assert!(cloud["products"].as_array().unwrap().iter().any(|p| p == "gunnar"), "{cloud}");
    assert_eq!((cloud["available"].as_bool(), cloud["provider"].as_str(), cloud["currency"].as_str()), (Some(true), Some("upcloud"), Some("EUR")), "{cloud}");
    // Every product (gunnar: alice, bob; skade: mira; holger and idun: none) lists the SAME account, so it is one entry
    // naming them all, with every product's rows on it once and its totals counted over them all.
    assert_eq!(cloud["products"], serde_json::json!(["gunnar", "holger", "idun", "skade"]), "{cloud}");
    let owned = cloud["owned"].as_array().unwrap();
    let tenants: Vec<&str> = owned.iter().map(|r| r["tenant"].as_str().unwrap()).collect();
    assert_eq!(tenants, ["alice", "bob", "mira"], "alice's and bob's disks, then skade's mira's: {cloud}");
    assert_eq!((owned[0]["id"].as_str(), owned[0]["gib"].as_u64(), owned[0]["tenant"].as_str()), (Some(fx::ALICE_DISK_ID), Some(10), Some("alice")));
    let gib: u64 = owned.iter().map(|r| r["gib"].as_u64().unwrap()).sum();
    assert_eq!(cloud["total_bytes"], n(gib * fx::GIB));
    assert_eq!(cloud["monthly_minor"], n(gib * 22), "the priced rows summed (22 a GiB in the fixture)");
    assert!(cloud["orphans"].as_array().unwrap().is_empty() && cloud["missing"].as_array().unwrap().is_empty());
}

/// (r) **Screen 2: every tenant row carries what the sort and the bars need,
/// and the outcomes behind RECENTLY BOUGHT.**
///
/// The BFF emits tenants ALPHABETICALLY — asserted, so nobody mistakes it for
/// the attention order the screen must produce.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn screen2_tenants_carry_meters_caps_state_and_the_fleet_for_the_sort() {
    let rig = Rig::up().await;
    let body = rig.get("/api/products/gunnar/tenants").await;
    let rows = body["tenants"].as_array().expect("rows");
    let names: Vec<&str> = rows.iter().map(|r| r["tenant"].as_str().unwrap()).collect();
    assert_eq!(names, fx::GUNNAR_TENANTS_ALPHABETICAL, "the server's order is by name; the console sorts by attention");
    assert_ne!(names, fx::GUNNAR_CLOSEST_FIRST);

    let row = |t: &str| rows.iter().find(|r| r["tenant"] == t).unwrap_or_else(|| panic!("no row for {t}"));
    // alice: every meter, its own number.
    let r = row("alice");
    assert_eq!(r["meters"]["pack_bytes"], n(fx::ALICE_PACK.raw));
    assert_eq!(r["meters"]["cache_bytes"], n(fx::ALICE_CACHE.raw));
    assert_eq!(r["meters"]["lfs_bytes"], n(fx::ALICE_LFS.raw));
    assert_eq!(r["meters"]["tombstoned_bytes"], n(fx::ALICE_TOMBSTONED.raw));
    assert_eq!(r["meters"]["repos"], n(fx::ALICE_REPOS));
    assert_eq!(r["meters"]["open_store_ram_bytes"], n(fx::ALICE_RAM.raw));
    // …and what the tenant is ALLOWED, on the row, so the sort needs no round trip.
    assert_eq!(r["caps"]["pack_bytes"], n(fx::CAP_10.raw), "the sort key: {r}");
    assert_eq!(r["caps"]["cache_bytes"], n(fx::CAP_CACHE_20.raw));
    assert_eq!((r["state"].as_str(), r["order"].as_str(), r["signed"].as_bool()), (Some("paid"), Some(fx::ALICE_REF), Some(true)), "a fact the mock really signed verifies, and names the order it came from");
    assert_eq!(r["measured_at_unix_ms"], n(fx::T0 + 5_000));
    // bjorn: no fact — free, no order, NO caps (not caps of zero).
    let b = row("bjorn");
    assert_eq!((b["state"].as_str(), b["order"].as_str()), (Some("free"), Some("")));
    assert!(b["caps"].as_object().is_some_and(|c| c.is_empty()), "uncapped is an EMPTY map: {b}");
    assert_eq!(b["meters"]["pack_bytes"], n(fx::BJORN_PACK.raw));
    // team/sub: grace, source ladder.
    assert_eq!((row("team/sub")["state"].as_str(), row("team/sub")["source"].as_str()), (Some("grace"), Some("ladder")));
    // edge: exactly at cap on the wire, so the screen's `AT CAP` at equality is testable.
    assert_eq!(row("edge")["meters"]["pack_bytes"], row("edge")["caps"]["pack_bytes"]);

    // The fleet and the rate travel with the rows.
    assert_eq!(body["fleet"]["growth"]["state"], "sealed");
    assert!(body["fleet"]["growth"]["detail"].as_str().unwrap().ends_with("not another disk"));
    assert_eq!(meter(&body["fleet"], "pack_bytes")["capacity"], n(fx::GUNNAR_CAPACITY.raw));
    assert_eq!(body["trend"]["meter"], "pack_bytes");
    assert_eq!(body["trend"]["per_week"].as_f64(), Some(fx::GUNNAR_FLEET_PER_WEEK));

    // RECENTLY BOUGHT: outcomes, one line per attempt, newest first, four endings.
    let sales = body["sales"].as_array().expect("sales");
    let seen: Vec<(&str, &str, bool, bool, u64)> = sales.iter().map(|s| (s["tenant"].as_str().unwrap(), s["outcome"].as_str().unwrap(), s["needs_you"].as_bool().unwrap(), s["drawn"].as_bool().unwrap(), s["price_minor"].as_u64().unwrap())).collect();
    assert_eq!(
        seen,
        vec![("stuck", "open", true, true, 22_000), ("bob", "granted", false, true, 22_000), ("zz-visibility", "refunded", false, true, 100), ("anna", "declined", false, false, 22_000)],
        "{sales:?}"
    );
    assert_eq!(sales[1]["caps"]["pack_bytes"], n(fx::CAP_10.raw), "`10 GiB for bob`");
    assert_eq!((sales[1]["currency"].as_str(), sales[1]["steps"].as_u64()), (Some("SEK"), Some(4)));
    assert_eq!(sales[0]["reason"], fx::STUCK_REASON, "the task row's reason, verbatim");
    assert_eq!(sales[2]["reason"], fx::ZZ_REFUND_REASON);
    let asked = rig.mock.calls.sales_asked.lock().unwrap().clone();
    assert_eq!(asked.len(), 1);
    assert_eq!(asked[0].product, "gunnar", "this product's sales only");
    assert!(asked[0].limit > 0, "a page, not the whole ledger");

    // An unknown product is a 404 in monetize's words, not an empty screen.
    let viewer = rig.cookie(Role::Viewer);
    let (status, body, _) = rig.call(Method::GET, "/api/products/loke/tenants", Auth::Cookie(&viewer), None).await;
    assert_eq!(status, StatusCode::NOT_FOUND, "{body}");
    assert!(body["message"].as_str().unwrap().contains("loke"), "{body}");
}

/// (r) **Screen 3: a tenant — every meter, the fact, what backs it, and its
/// own rate.**
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn screen3_tenant_detail_carries_every_meter_the_fact_the_holding_and_its_own_rate() {
    let rig = Rig::up().await;
    let d = rig.get("/api/tenants/gunnar/alice").await;
    for (name, want) in [("pack_bytes", fx::ALICE_PACK.raw), ("cache_bytes", fx::ALICE_CACHE.raw), ("lfs_bytes", fx::ALICE_LFS.raw), ("tombstoned_bytes", fx::ALICE_TOMBSTONED.raw), ("repos", fx::ALICE_REPOS), ("open_store_ram_bytes", fx::ALICE_RAM.raw)] {
        assert_eq!(d["row"]["meters"][name], n(want), "{name}");
    }
    // The meters come with their words: enforcement for `wall` / `not billed` /
    // `still costs`, and the meaning for the detail.
    let m: Vec<(&str, &str)> = d["meters"].as_array().unwrap().iter().map(|m| (m["name"].as_str().unwrap(), m["enforcement"].as_str().unwrap())).collect();
    assert!(m.contains(&("pack_bytes", "enforced")) && m.contains(&("lfs_bytes", "measured")) && m.contains(&("tombstoned_bytes", "measured")), "{m:?}");
    // The fact: what is ALLOWED, verified.
    assert_eq!(d["fact"]["caps"]["pack_bytes"], n(fx::CAP_10.raw));
    assert_eq!((d["fact"]["state"].as_str(), d["fact"]["signed"].as_bool()), (Some("paid"), Some(true)));
    assert!(d["fact"]["signature"].is_null(), "the verdict travels, not the bytes");
    // BACKED BY: what was BOUGHT, distinct from what is allowed.
    let h = d["holdings"].as_array().expect("holdings");
    assert_eq!(h.len(), 1);
    assert_eq!((h[0]["kind"].as_str(), h[0]["detail"].as_str(), h[0]["provisioned"].as_bool()), (Some("disk"), Some("10 GiB on gunnar-se-sto1"), Some(true)));
    assert_eq!(h[0]["id"], format!("upcloud:{}", fx::ALICE_DISK_ID));
    assert_eq!(h[0]["reference"], fx::ALICE_REF);
    // Its own rate: firm, so no tilde, and projectable.
    let t = &d["trend"];
    assert_eq!((t["known"].as_bool(), t["per_week"].as_f64(), t["marker"].as_str(), t["may_project"].as_bool()), (Some(true), Some(fx::ALICE_PER_WEEK), Some(""), Some(true)), "{t}");
    let asked = rig.mock.calls.trends_asked.lock().unwrap().clone();
    assert_eq!((asked[0].tenant.as_str(), asked[0].meter.as_str()), ("alice", "pack_bytes"), "THIS tenant's rate, on the wall meter");
    // History: the steps behind the purchase that granted the disk.
    let kinds: Vec<&str> = d["rows"].as_array().unwrap().iter().map(|r| r["kind"].as_str().unwrap()).collect();
    assert_eq!(kinds, ["Reserved", "Ensured", "Captured", "EntitlementPushed"]);
    assert_eq!(d["references"], serde_json::json!([fx::ALICE_REF]));
    // THE TERMS the order form prices by: gunnar's SE list, and the policy.
    assert_eq!(d["prices"].as_array().unwrap().len(), 1, "this product's lists only: {}", d["prices"]);
    assert_eq!((d["prices"][0]["market"].as_str(), d["prices"][0]["currency"].as_str()), (Some("SE"), Some("SEK")));
    assert_eq!(d["prices"][0]["units"]["disk_gib_month"].as_u64(), Some(fx::GUNNAR_DISK_GIB_MONTH_MINOR));
    assert_eq!(d["prices"][0]["units"]["cpu_core_month"].as_u64(), Some(0), "a zero unit is on the wire as zero: free, said");
    assert_eq!((d["policy"]["period"].as_str(), d["policy"]["grace_days"].as_u64()), (Some("month"), Some(14)));
    // Every meter says what unit it is priced by, or that it has none.
    let unit_of = |name: &str| d["meters"].as_array().unwrap().iter().find(|m| m["name"] == name).unwrap()["unit"].as_str().unwrap().to_owned();
    assert_eq!((unit_of("pack_bytes").as_str(), unit_of("tombstoned_bytes").as_str(), unit_of("cache_bytes").as_str(), unit_of("repos").as_str()), ("disk_gib_month", "disk_gib_month", "", ""));
    assert_eq!(d["vendors"], serde_json::json!(fx::VENDORS));

    // bjorn: no fact, no holding, not enough history — three absences, each its own null.
    let d = rig.get("/api/tenants/gunnar/bjorn").await;
    assert!(d["fact"].is_null());
    assert_eq!(d["holdings"], serde_json::json!([]));
    assert_eq!(d["row"]["meters"]["pack_bytes"], n(fx::BJORN_PACK.raw));
    assert_eq!(d["trend"]["known"], false);
    assert!(d["trend"]["per_week"].is_null(), "{}", d["trend"]);

    // A tenant nobody has: the shape says so with nulls, not with zeros.
    let d = rig.get("/api/tenants/gunnar/nobody").await;
    assert!(d["row"].is_null() && d["fact"].is_null(), "{d}");

    // skade/mira: a bursty tenant rate — the figure, no date.
    let d = rig.get("/api/tenants/skade/mira").await;
    assert_eq!((d["trend"]["confidence"].as_str(), d["trend"]["may_project"].as_bool()), (Some("bursty"), Some(false)));
    assert_eq!(d["row"]["meters"]["store_bytes"], n(fx::MIRA_STORE.raw));
}

/// (r) **Screen 4: the ledger leads with outcomes; the open attempt is the only
/// task; the steps are still there for the disclosure.**
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn screen4_ledger_leads_with_outcomes_and_keeps_the_steps_behind_them() {
    let rig = Rig::up().await;
    let l = rig.get("/api/ledger?product=gunnar").await;
    let sales = l["sales"].as_array().expect("outcomes");
    assert_eq!(sales.len(), 4, "one line per attempt, not one per step");
    let tasks: Vec<&str> = sales.iter().filter(|s| s["needs_you"] == true).map(|s| s["tenant"].as_str().unwrap()).collect();
    assert_eq!(tasks, ["stuck"], "exactly one row is a task");
    assert_eq!(sales.iter().find(|s| s["tenant"] == "stuck").unwrap()["reason"], fx::STUCK_REASON);
    // Refunded and declined are DIFFERENT endings: one drew, one never locked.
    let zz = sales.iter().find(|s| s["tenant"] == "zz-visibility").unwrap();
    let anna = sales.iter().find(|s| s["tenant"] == "anna").unwrap();
    assert_eq!((zz["outcome"].as_str(), zz["drawn"].as_bool()), (Some("refunded"), Some(true)));
    assert_eq!((anna["outcome"].as_str(), anna["drawn"].as_bool()), (Some("declined"), Some(false)));
    assert_eq!(anna["reason"], fx::ANNA_DECLINE_REASON);
    // The open attempt, with its rows, for the checkout link and the timeline.
    assert_eq!(l["unfinished"][0]["reference"], fx::STUCK_REF);
    let kinds: Vec<&str> = l["unfinished"][0]["rows"].as_array().unwrap().iter().map(|r| r["kind"].as_str().unwrap()).collect();
    assert_eq!(kinds, ["Reserved", "Captured"], "paid, never delivered");
    // The steps, newest first across the whole ledger — the disclosure's data.
    let recent = l["recent"].as_array().unwrap();
    assert_eq!((recent[0]["reference"].as_str(), recent[0]["kind"].as_str()), (Some(fx::STUCK_REF), Some("Captured")));
    assert_eq!(recent.len(), fx::rows().len());
    let bob_steps: Vec<&str> = recent.iter().filter(|r| r["reference"] == fx::BOB_REF).map(|r| r["kind"].as_str().unwrap()).collect();
    assert_eq!(bob_steps, ["EntitlementPushed", "Captured", "Ensured", "Reserved"]);
    let asked = rig.mock.calls.sales_asked.lock().unwrap().clone();
    assert_eq!(asked[0].product, "gunnar");

    // No product: every product's attempts, still newest first.
    let all = rig.get("/api/ledger").await;
    let tenants: Vec<&str> = all["sales"].as_array().unwrap().iter().map(|s| s["tenant"].as_str().unwrap()).collect();
    assert_eq!(tenants, ["stuck", "bob", "mira", "zz-visibility", "anna"], "skade's sale is interleaved by time");
}

/// (r) **Screen 5: Tax — the periods a year is filed in, and one filled form.**
///
/// The arithmetic lives in `monetize::tax` and in the jurisdiction impl and is
/// tested there. What this asserts is the WIRING that nothing else can: that
/// the two routes exist and reach the two RPCs, and that a box nobody computed
/// survives every hop as ABSENT rather than arriving as `0`. The fixture's box
/// 20 is that box, deliberately — a report with a figure in every line could
/// not tell `—` from `0.00`, and `0.00` in a tax return is a claim.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn screen5_tax_lists_the_periods_and_fills_the_boxes_without_inventing_a_zero() {
    let rig = Rig::up().await;

    let p = rig.get(&format!("/api/tax/periods?year={}", fx::TAX_YEAR)).await;
    assert_eq!(p["jurisdiction"], fx::TAX_JURISDICTION);
    let labels: Vec<&str> = p["periods"].as_array().expect("periods").iter().map(|x| x["label"].as_str().unwrap()).collect();
    assert_eq!(labels, ["2026-Q1", "2026-Q2", "2026-Q3", "2026-Q4"]);
    assert_eq!(p["periods"][0]["kind"], "quarterly");
    // The evidence for filing quarterly and not twelve times: the previous
    // year's turnover, on the response so the page can show it.
    assert_eq!(p["turnover_minor"], n(fx::TAX_TURNOVER_MINOR));
    assert_eq!((p["currency"].as_str(), p["turnover_year"].as_u64()), (Some("SEK"), Some(fx::TAX_TURNOVER_YEAR as u64)));

    let r = rig.get(&format!("/api/tax/report?year={}&period=2026-Q1", fx::TAX_YEAR)).await;
    assert_eq!((r["jurisdiction"].as_str(), r["period"]["label"].as_str()), (Some("SE"), Some("2026-Q1")));
    let lines = r["lines"].as_array().expect("lines");
    assert_eq!(lines.len(), fx::tax_lines().len());
    let box_of = |id: &str| lines.iter().find(|l| l["box_id"] == id).unwrap_or_else(|| panic!("no box {id} in {r}"));
    assert_eq!(box_of("05")["amount_minor"], n(150_000));
    assert_eq!(box_of("05")["label"], "Momspliktig försäljning som inte ingår i fält 06, 07 eller 08");
    assert_eq!(box_of("10")["amount_minor"], n(37_500));
    // THE ONE THAT MATTERS: absent all the way, never a zero.
    assert_eq!(box_of("20")["amount_minor"], Value::Null, "a box nobody computed must not arrive as 0");
    assert_eq!(box_of("20")["currency"], "");
    // The reverse charge, owed and deducted, the same figure in both boxes.
    assert_eq!(box_of("30")["amount_minor"], box_of("48")["amount_minor"]);
    assert_eq!(r["notes"], serde_json::json!(fx::tax_notes()));
    // The accountant's file rides along, rendered by monetize and not by the
    // console, so the table and the download cannot disagree.
    let csv = r["csv"].as_str().expect("csv");
    assert!(csv.starts_with("box,label,amount,currency\n"), "{csv}");
    assert!(csv.contains("20,Inköp av varor från annat EU-land,—,"), "the dash is in the file too: {csv}");
    assert!(csv.contains("\"Inköp av tjänster från annat EU-land, enligt huvudregeln\""), "a comma-bearing label is quoted: {csv}");
}

/// A period the deployment does not file is refused, and a year that is not a
/// year never leaves the console's server. Both are 4xx and both say which
/// parameter — a tax return for the wrong period is worse than no tax return.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_period_nobody_files_and_a_year_that_is_not_a_year_are_both_refused_by_name() {
    let rig = Rig::up().await;
    let viewer = rig.cookie(Role::Viewer);

    let (status, body, _) = rig.call(Method::GET, "/api/tax/report?year=2026&period=2026-Q9", Auth::Cookie(&viewer), None).await;
    assert_eq!(status, StatusCode::NOT_FOUND, "{body}");
    assert!(body["message"].as_str().unwrap_or_default().contains("2026-Q9"), "{body}");

    let (status, body, _) = rig.call(Method::GET, "/api/tax/report?year=2026", Auth::Cookie(&viewer), None).await;
    assert_eq!(status, StatusCode::BAD_REQUEST, "{body}");
    assert!(body["message"].as_str().unwrap_or_default().contains("period is required"), "{body}");

    let (status, body, _) = rig.call(Method::GET, "/api/tax/periods?year=last", Auth::Cookie(&viewer), None).await;
    assert_eq!(status, StatusCode::BAD_REQUEST, "{body}");
    assert!(body["message"].as_str().unwrap_or_default().contains("\"last\""), "{body}");
}

/// ★ **RA3-1 / PROD-4 follow-up: the PRODUCTS page answers within the 2 s page rule with the
/// cloud listing stuck** — through the console's real router over the mock monetize, not only
/// the unit test of the bound. A first look reads the fleet; then `Usage.Fleet` never answers,
/// and the next look still answers within the bound, from the fleet it last read, saying its
/// age (`stale_secs`).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_products_page_answers_within_the_page_rule_while_the_cloud_listing_is_stuck() {
    let rig = Rig::up().await;
    let first = rig.get("/api/products").await;
    assert!(first["products"][0]["fleet"]["cloud"].is_object(), "{first}");
    rig.mock.fleet_stuck.store(true, std::sync::atomic::Ordering::SeqCst);
    let t0 = std::time::Instant::now();
    let body = rig.get("/api/products").await;
    let took = t0.elapsed();
    assert!(took < std::time::Duration::from_millis(2_500), "the products page waited {took:?} on a stuck cloud listing");
    let fleet = &body["products"][0]["fleet"];
    assert!(fleet["stale_secs"].is_u64(), "the last fleet read, aged: {fleet}");
    assert_eq!(fleet["cloud"]["total_bytes"], first["products"][0]["fleet"]["cloud"]["total_bytes"]);
}