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

nordisk/monetize

crates/monetize-site-buy/src/relay.rs

//! **The relay: the site's own buy doors, over the shop.**
//!
//! Each door is a plain async function — `(relay, ask, body) → Answer` — so a site with its
//! own router and extractors (Gunnar's console) calls them from its handlers, and a site
//! without one mounts [`router`]. The JSON contract is Gunnar's, path for path; a refusal is
//! `{"error": "<sentence>"}`.
//!
//! | door | shop call |
//! |---|---|
//! | `GET /auth/signup/ladder[?market=]` | `Signup.Plans` |
//! | `GET /auth/signup/name?u=` | `Signup.CheckName` |
//! | `POST /auth/signup/start` | `Signup.Start`, the ways back on the site |
//! | `GET /auth/signup/status?session=` | `Signup.Status` (+ the product's account) |
//! | `POST /auth/signup/rename` | `Signup.Rename` |
//! | `POST /auth/signup/sign-in-again` | `Signup.SendSignInAgain` |
//! | `GET /auth/private/ladder[?company=]` | `EstateOrder.Ladder` |
//! | `POST /auth/private/start` | `EstateOrder.Start` (a visitor pays once; signed in: the account's `estate` ticket, no e-mail) |
//! | `POST /auth/private/change` | `EstateOrder.ChangePlan` |
//! | `GET /auth/private/status?company=` | `EstateOrder.Status` |
//! | `POST /auth/private/sign-in-again` | `EstateOrder.SendSignInAgain` |
//! | `POST /auth/private/renew {company}` | `EstateOrder.Renew` (Renew <co>) |
//! | `POST /auth/private/cancel {company}` | `EstateOrder.Cancel` (Stop renewing) |
//! | `POST /auth/private/terminate {company, confirm}` | `EstateOrder.Terminate` (a fresh login, the name typed back) |
//! | `POST /auth/private/restore-front {company}` | `EstateOrder.RestoreFront` |
//! | `GET /paid/estate/{co}[/{change}]` | none: 303 to the order's watch on the site |
//! | `GET /api/account/subscription/bundles` | `Purchase.Bundles` (the account's grow) |
//! | `POST /api/account/subscription/buy-bundle` | `Purchase.Start` with the bundle |
//! | `GET /api/account/subscription/status?reference=` | `Purchase.Status` (the way back from the vendor) |
//! | `GET /api/account/subscription` | `Purchase.Quote{renew}` (what another period comes to) |
//! | `POST /api/account/subscription/renew` | `Purchase.Start{renew}` (Prolong) |
//! | `GET /api/account/subscription/history` | `Purchase.History` (Past purchases) |
//! | `POST /api/account/subscription/cancel {reference}` | `Purchase.Cancel` ("Cancel order", an order not yet paid) |
//! | `GET /api/account/purchases` | `EstateOrder.Purchases` (Your purchases: the instances, Grow, Manage) |
//! | `GET /auth/buy/offer` | who asks, and what the shop sells here (`/auth/status`'s shop fields) |

use std::collections::{BTreeMap, HashMap};
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;

use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use monetize_proto as pb;
use serde_json::{json, Value};

use crate::link::ShopLink;
use crate::rules::{self, words_for_a_visitor, RULE_IN_WORDS};
use crate::site::{Account, BuySite, Buyer, Refusal, Who, PURPOSE_CANCEL, STEP_UP_CODE, PURPOSE_ESTATE, PURPOSE_ORDER, PURPOSE_READ, PURPOSE_RENEW};
use crate::urls;

/// The `tracing` target every audit line of a buy lands on (route it with
/// `RUST_LOG=monetize_site_buy::audit=info`). The events are Gunnar's console's, by name.
pub const AUDIT_TARGET: &str = "monetize_site_buy::audit";

/// What a buyer reads when the ONE box does not answer the plan list.
pub const SHOP_UNAVAILABLE: &str = "The shop is unavailable right now — try again in a few minutes.";
/// What a buyer reads where the shop sells no account here.
pub const ACCOUNT_NOT_SOLD_HERE: &str = "An account is not sold here right now.";
/// What a buyer reads when monetize did not mint a checkout in time.
pub const START_TOO_SLOW: &str = "The payment page did not open in time. Nothing has been charged — please try again in a minute.";
/// A session id that is not one.
pub const NOT_A_SESSION: &str = "that is not a checkout session this console can ask about";

/// **The relay**: the product's site and its link to the shop.
pub struct Relay<S> {
    pub site: S,
    pub shop: ShopLink,
}

impl<S: BuySite> Relay<S> {
    pub fn new(site: S, shop: ShopLink) -> Relay<S> {
        Relay { site, shop }
    }
}

/// **What a door was asked with**: the request's headers (the person's session, the browser's
/// languages) and the client's address as the site knows it.
#[derive(Clone, Debug, Default)]
pub struct Ask {
    pub headers: HeaderMap,
    pub ip: String,
}

impl Ask {
    pub fn new(headers: HeaderMap, ip: impl Into<String>) -> Ask {
        Ask { headers, ip: ip.into() }
    }

    fn accept_language(&self) -> Option<&str> {
        self.headers.get(axum::http::header::ACCEPT_LANGUAGE).and_then(|v| v.to_str().ok())
    }
}

/// **A door's answer**: JSON with a status, or a 303 to a path on the site.
#[derive(Clone, Debug, PartialEq)]
pub struct Answer {
    pub status: StatusCode,
    pub body: Value,
    /// A `303 See Other` to this path instead of a body.
    pub see_other: Option<String>,
}

impl Answer {
    pub fn ok(body: Value) -> Answer {
        Answer { status: StatusCode::OK, body, see_other: None }
    }

    pub fn refuse(status: StatusCode, words: &str) -> Answer {
        Answer { status, body: json!({ "error": words }), see_other: None }
    }

    pub fn see_other(to: String) -> Answer {
        Answer { status: StatusCode::SEE_OTHER, body: Value::Null, see_other: Some(to) }
    }

    fn of(r: Refusal) -> Answer {
        Answer::refuse(r.status, &r.words)
    }

    /// The refusal's sentence, when this is one.
    pub fn error(&self) -> Option<&str> {
        self.body.get("error").and_then(Value::as_str)
    }
}

impl IntoResponse for Answer {
    fn into_response(self) -> Response {
        match self.see_other {
            Some(to) => (StatusCode::SEE_OTHER, [(axum::http::header::LOCATION, to), (axum::http::header::CACHE_CONTROL, "no-store".to_owned())]).into_response(),
            None => (self.status, [(axum::http::header::CACHE_CONTROL, "no-store")], Json(self.body)).into_response(),
        }
    }
}

pub(crate) fn now_ms() -> u64 {
    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0)
}

// ── sign-up ────────────────────────────────────────────────────────────────────────────

/// `GET /auth/signup/ladder[?market=]` → `{plans:[{id, name, resources_words, price_words,
/// period_days}], market}`: what a new account costs, in ONE market. A shop that does not
/// answer: 503 with the last offer it gave, read only.
pub async fn signup_plans<S: BuySite>(r: &Relay<S>, ask: &Ask, market: Option<&str>) -> Answer {
    let market = match rules::market_for(market, ask.accept_language()) {
        Ok(m) => m,
        Err(why) => return Answer::refuse(StatusCode::BAD_REQUEST, &why),
    };
    if !r.site.allowed("private", &ask.ip, "plans") {
        return Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many requests from here; wait a moment");
    }
    // From the site's kept ladders (LADDER-2): never a wait on the ONE box past the first read.
    r.shop.caches().ladders().answer(&r.shop, &crate::ladders::signup_key(market as i32)).await
}

/// The account plans page's body of a `Signup.Plans` answer.
pub(crate) fn plans_body(a: pb::SignupPlansResponse) -> Value {
    json!({
        "plans": a.plans.iter().map(|p| json!({
            "id": p.id, "name": p.name, "resources_words": p.resources_words,
            "price_words": p.price_words, "period_days": p.period_days,
        })).collect::<Vec<_>>(),
        "market": rules::market_word(a.market),
    })
}

/// `GET /auth/signup/name?u=` → `{free, reason}`.
pub async fn signup_name<S: BuySite>(r: &Relay<S>, ask: &Ask, name: &str) -> Answer {
    let name = name.trim();
    if !rules::username(name) {
        return Answer::ok(json!({ "free": false, "reason": format!("a username is {RULE_IN_WORDS}") }));
    }
    if !r.site.allowed("signup_name", &ask.ip, name) {
        tracing::warn!(target: AUDIT_TARGET, event = "signup.name_rate_limited", ip = %ask.ip, "a signup name check was rate-limited");
        return Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many names checked from here; wait a moment");
    }
    match r.shop.signup().check_name(pb::CheckNameRequest { username: name.to_owned(), product: r.shop.product().to_owned() }).await {
        Ok(a) => {
            let a = a.into_inner();
            Answer::ok(json!({ "free": a.free, "reason": a.reason }))
        }
        Err(s) => {
            tracing::warn!(target: AUDIT_TARGET, event = "signup.name_failed", code = ?s.code(), message = %s.message(), "monetize could not answer a signup name check");
            Answer::refuse(StatusCode::BAD_GATEWAY, words_for_a_visitor(&s))
        }
    }
}

/// What `POST /auth/signup/start` is given (Gunnar's `StartBody`).
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct SignupStart {
    pub username: String,
    #[serde(default)]
    pub email: String,
    #[serde(default)]
    pub email_confirm: String,
    #[serde(default)]
    pub market: Option<String>,
    #[serde(default)]
    pub plan: String,
    #[serde(default)]
    pub next: String,
}

/// The member's refusal of a sign-up.
fn member_buys_nothing(m: &crate::site::Member) -> String {
    format!("{} A member buys nothing: {}'s room is its admin's to buy.", m.line, m.account)
}

/// `POST /auth/signup/start` → `{url}`: a checkout for a new account (the free account's card
/// check, a paid plan). The e-mail is asked here, once, at account creation.
pub async fn signup_start<S: BuySite>(r: &Relay<S>, ask: &Ask, body: &SignupStart) -> Answer {
    match r.site.who(&ask.headers).await {
        Ok(Who::Person(Buyer { member: Some(m), .. })) => return Answer::refuse(StatusCode::FORBIDDEN, &member_buys_nothing(&m)),
        Ok(_) => {}
        Err(why) => return Answer::of(why),
    }
    let name = body.username.trim();
    if !rules::username(name) {
        return Answer::refuse(StatusCode::BAD_REQUEST, &format!("a username is {RULE_IN_WORDS}"));
    }
    let email = body.email.trim();
    let confirm = if body.email_confirm.trim().is_empty() { email } else { body.email_confirm.trim() };
    if let Some(why) = rules::email_refusal(email, confirm) {
        return Answer::refuse(StatusCode::BAD_REQUEST, why);
    }
    let plan = body.plan.trim();
    if !rules::plan_id(plan) {
        return Answer::refuse(StatusCode::BAD_REQUEST, "that is not a plan this console can ask for");
    }
    let market = match rules::market(body.market.as_deref()) {
        Ok(m) => m,
        Err(why) => return Answer::refuse(StatusCode::BAD_REQUEST, &why),
    };
    if !r.site.allowed("signup_name", &ask.ip, name) {
        tracing::warn!(target: AUDIT_TARGET, event = "signup.start_rate_limited", ip = %ask.ip, "a signup checkout request was rate-limited");
        return Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many signup attempts from here; wait a moment");
    }
    let began = std::time::Instant::now();
    let origin = r.site.origin();
    let origin = origin.trim_end_matches('/');
    let next = rules::safe_next(body.next.trim());
    let mut client = r.shop.signup_minting();
    let asked = client.start(pb::StartSignupRequest {
        username: name.to_owned(),
        product: r.shop.product().to_owned(),
        plan: plan.to_owned(),
        vendor: r.shop.vendor().to_owned(),
        success_url: urls::signup_success(origin, next.as_deref()),
        cancel_url: urls::signup_cancel(origin, body.next.trim(), &r.site.pages()),
        email: email.to_owned(),
        market: market as i32,
    });
    let deadline = r.shop.deadlines().mint;
    let started = match tokio::time::timeout(deadline, asked).await {
        Ok(answer) => answer,
        Err(_) => {
            tracing::warn!(target: AUDIT_TARGET, event = "signup.start_timed_out", user = %name, waited_ms = began.elapsed().as_millis() as u64, deadline_ms = deadline.as_millis() as u64, "monetize minted no signup checkout within the deadline; the buyer was told nothing was charged");
            return Answer::refuse(StatusCode::GATEWAY_TIMEOUT, START_TOO_SLOW);
        }
    };
    let monetize_ms = began.elapsed().as_millis() as u64;
    match started {
        Ok(a) => {
            let url = a.into_inner().url;
            let own_welcome = url.starts_with(&format!("{origin}/welcome?"));
            if rules::https_url(&url).is_none() && !own_welcome {
                tracing::error!(target: AUDIT_TARGET, event = "signup.start_bad_url", "monetize answered a checkout URL that is not https");
                return Answer::refuse(StatusCode::BAD_GATEWAY, "the checkout address is not one this console will send a browser to");
            }
            tracing::info!(target: AUDIT_TARGET, event = "signup.start", user = %name, monetize_ms, "a signup checkout was minted");
            Answer::ok(json!({ "url": url }))
        }
        Err(s) if s.code() == tonic::Code::Unimplemented => match r.site.fixed_buy_url() {
            // The typed address rides along as the link's pre-filled e-mail.
            Some(fixed) => {
                tracing::info!(target: AUDIT_TARGET, event = "signup.start_fallback", "monetize mints no checkout; the configured buy link was used");
                Answer::ok(json!({ "url": buy_url_for(&fixed, email) }))
            }
            None => {
                tracing::warn!(target: AUDIT_TARGET, event = "signup.not_sold", "this site cannot sell an account: monetize mints no checkout and no fixed buy link is configured");
                Answer::refuse(StatusCode::NOT_IMPLEMENTED, ACCOUNT_NOT_SOLD_HERE)
            }
        },
        Err(s) => {
            tracing::warn!(target: AUDIT_TARGET, event = "signup.start_failed", code = ?s.code(), monetize_ms, message = %s.message(), "monetize would not mint a signup checkout");
            let code = match s.code() {
                tonic::Code::FailedPrecondition | tonic::Code::InvalidArgument => StatusCode::CONFLICT,
                tonic::Code::PermissionDenied => StatusCode::FORBIDDEN,
                _ => StatusCode::BAD_GATEWAY,
            };
            Answer::refuse(code, words_for_a_visitor(&s))
        }
    }
}

/// The fixed buy link with the typed address pre-filled (Stripe Payment Links read
/// `prefilled_email`). A link that already carries a query keeps it.
pub fn buy_url_for(fixed: &str, email: &str) -> String {
    let sep = if fixed.contains('?') { '&' } else { '?' };
    format!("{fixed}{sep}prefilled_email={}", rules::query_value(email))
}

fn state_name(wire: i32) -> &'static str {
    use pb::signup_status_response::State;
    match State::try_from(wire) {
        Ok(State::Pending) => "pending",
        Ok(State::Created) => "created",
        Ok(State::Refused) => "refused",
        _ => "unknown",
    }
}

fn renameable(wire: i32) -> bool {
    matches!(pb::signup_status_response::Refusal::try_from(wire), Ok(pb::signup_status_response::Refusal::Name))
}

/// A sign-up that is not an account (yet): monetize's customer sentence and nothing else.
fn not_created(a: &pb::SignupStatusResponse) -> Answer {
    if !a.reason.is_empty() {
        tracing::info!(target: AUDIT_TARGET, event = "signup.not_created", state = state_name(a.state), reason = %a.reason, "monetize's operator reason for a signup that is not an account");
    }
    // RA4-19: a PENDING answer carries the box's sentence for its own silence, and when it is
    // due, so the page keeps no settle bound and no settle words of its own.
    Answer::ok(json!({ "state": state_name(a.state), "username": "", "invite_url": Value::Null, "words": a.words, "renameable": renameable(a.refusal), "words_if_silent": a.words_if_silent, "silent_after_ms": a.silent_after_ms }))
}

/// The account exists: the link that registers its passkey, while it holds none.
async fn created<S: BuySite>(r: &Relay<S>, session: &str, a: &pb::SignupStatusResponse) -> Answer {
    let product = r.shop.product();
    if !a.product.is_empty() && a.product != product {
        return Answer::ok(json!({ "state": "refused", "username": "", "invite_url": Value::Null, "words": format!("that order is for {:?} and this console is {product}'s", a.product), "renameable": false }));
    }
    let user = a.username.trim();
    match r.site.signup_account(user).await {
        Ok(Account::NoPasskey) => {
            tracing::info!(target: AUDIT_TARGET, event = "signup.link", user = %user, "a paid signup's account is ready for its passkey");
            Answer::ok(json!({ "state": "created", "username": a.username, "invite_url": urls::invite_url(session, user), "words": "", "renameable": false }))
        }
        Ok(Account::HasPasskey) => Answer::ok(json!({ "state": "created", "username": a.username, "invite_url": Value::Null, "words": "this signup's registration link has already been used", "renameable": false })),
        Err(why) => {
            tracing::error!(target: AUDIT_TARGET, event = "signup.link_failed", user = %user, reason = %why.words, "an account exists but the site could not ready its registration");
            Answer::of(why)
        }
    }
}

/// `GET /auth/signup/status?session=` → `{state, username, invite_url, words, renameable}`, and
/// for PENDING `{words_if_silent, silent_after_ms}` (RA4-19).
pub async fn signup_status<S: BuySite>(r: &Relay<S>, ask: &Ask, session: &str) -> Answer {
    if !rules::session(session) {
        return Answer::refuse(StatusCode::BAD_REQUEST, NOT_A_SESSION);
    }
    if !r.site.allowed("signup", &ask.ip, session) {
        tracing::warn!(target: AUDIT_TARGET, event = "signup.rate_limited", ip = %ask.ip, "a signup status poll was rate-limited");
        return Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many signup lookups from here; wait a moment");
    }
    let a = match r.shop.signup().status(pb::SignupStatusRequest { vendor: r.shop.vendor().to_owned(), session: session.to_owned() }).await {
        Ok(a) => a.into_inner(),
        Err(s) => {
            tracing::warn!(target: AUDIT_TARGET, event = "signup.status_failed", code = ?s.code(), message = %s.message(), "monetize could not answer a signup status poll");
            return Answer::refuse(StatusCode::BAD_GATEWAY, words_for_a_visitor(&s));
        }
    };
    if state_name(a.state) != "created" {
        return not_created(&a);
    }
    created(r, session, &a).await
}

/// What `POST /auth/signup/rename` is given.
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct SignupRename {
    pub session: String,
    pub username: String,
}

/// `POST /auth/signup/rename` → the status's own answer: a sign-up refused over its NAME,
/// finished under another one.
pub async fn signup_rename<S: BuySite>(r: &Relay<S>, ask: &Ask, body: &SignupRename) -> Answer {
    let (session, name) = (body.session.trim(), body.username.trim());
    if !rules::session(session) {
        return Answer::refuse(StatusCode::BAD_REQUEST, NOT_A_SESSION);
    }
    if !rules::username(name) {
        return Answer::refuse(StatusCode::BAD_REQUEST, &format!("a username is {RULE_IN_WORDS}"));
    }
    if !r.site.allowed("signup", &ask.ip, session) {
        tracing::warn!(target: AUDIT_TARGET, event = "signup.rename_rate_limited", ip = %ask.ip, "a signup rename was rate-limited");
        return Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many signup attempts from here; wait a moment");
    }
    let a = match r.shop.signup().rename(pb::RenameSignupRequest { vendor: r.shop.vendor().to_owned(), session: session.to_owned(), username: name.to_owned() }).await {
        Ok(a) => a.into_inner().status.unwrap_or_default(),
        Err(s) => {
            tracing::warn!(target: AUDIT_TARGET, event = "signup.rename_failed", code = ?s.code(), user = %name, message = %s.message(), "monetize would not rename a signup");
            let code = match s.code() {
                tonic::Code::InvalidArgument | tonic::Code::FailedPrecondition => StatusCode::CONFLICT,
                tonic::Code::NotFound => StatusCode::NOT_FOUND,
                tonic::Code::PermissionDenied => StatusCode::FORBIDDEN,
                _ => StatusCode::BAD_GATEWAY,
            };
            return Answer::refuse(code, words_for_a_visitor(&s));
        }
    };
    if state_name(a.state) != "created" {
        return not_created(&a);
    }
    tracing::info!(target: AUDIT_TARGET, event = "signup.renamed", user = %a.username, "a signup refused over its name was finished under another one");
    created(r, session, &a).await
}

/// What `POST /auth/signup/sign-in-again` is given.
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct SignInAgain {
    #[serde(default)]
    pub session: String,
    #[serde(default)]
    pub next: String,
}

/// `POST /auth/signup/sign-in-again` → `{words}`: the welcome page's [Send again].
pub async fn signup_sign_in_again<S: BuySite>(r: &Relay<S>, ask: &Ask, body: &SignInAgain) -> Answer {
    let session = body.session.trim();
    if !rules::session(session) {
        return Answer::refuse(StatusCode::BAD_REQUEST, NOT_A_SESSION);
    }
    if !r.site.allowed("signup", &ask.ip, session) {
        return Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many signup lookups from here; wait a moment");
    }
    let failed = |s: tonic::Status| {
        let code = match s.code() {
            tonic::Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
            tonic::Code::NotFound | tonic::Code::FailedPrecondition => StatusCode::CONFLICT,
            tonic::Code::InvalidArgument => StatusCode::BAD_REQUEST,
            tonic::Code::PermissionDenied => StatusCode::FORBIDDEN,
            _ => StatusCode::BAD_GATEWAY,
        };
        let words = if s.code() == tonic::Code::ResourceExhausted { s.message() } else { words_for_a_visitor(&s) };
        Answer::refuse(code, words)
    };
    let a = match r.shop.signup().status(pb::SignupStatusRequest { vendor: r.shop.vendor().to_owned(), session: session.to_owned() }).await {
        Ok(a) => a.into_inner(),
        Err(s) => return failed(s),
    };
    if state_name(a.state) != "created" {
        return Answer::refuse(StatusCode::CONFLICT, "This account is not made yet: its sign-in link is mailed once it is.");
    }
    match r.site.signup_account(a.username.trim()).await {
        Ok(Account::NoPasskey) => {}
        Ok(Account::HasPasskey) => return Answer::refuse(StatusCode::CONFLICT, "This account already has a security key: sign in with it."),
        Err(why) => return Answer::of(why),
    }
    let next = rules::safe_next(body.next.trim());
    let origin = r.site.origin();
    match r.shop.signup().send_sign_in_again(pb::SignupSignInAgainRequest { product: r.shop.product().to_owned(), session: session.to_owned(), welcome_url: urls::signup_success(&origin, next.as_deref()) }).await {
        Ok(a) => Answer::ok(json!({ "words": a.into_inner().words })),
        Err(s) => failed(s),
    }
}

// ── a private instance ─────────────────────────────────────────────────────────────────

/// The most bytes of a product's own order a page may send (its JSON).
pub const ORDER_MAX: usize = 4096;

/// What a private-instance door is given (Gunnar's `SizesBody`).
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct Sizes {
    #[serde(default)]
    pub company: String,
    /// A visitor's: the new account's address. Signed in, none is sent: monetize mails the
    /// account's own.
    #[serde(default)]
    pub email: String,
    #[serde(default)]
    pub domain: String,
    #[serde(default)]
    pub front_plan: String,
    #[serde(default)]
    pub main_plan: String,
    #[serde(default)]
    pub main_data_gib: u64,
    /// ★ **The product's own order** (v2.0 P3, the parametric order): the values its order form
    /// names (a JSON object), passed on as they came; the ONE box checks them against the
    /// product's form. Absent: the product's default order.
    #[serde(default)]
    pub order: serde_json::Value,
    /// A visitor's: the name the NEW account is to have. Signed in: ignored.
    #[serde(default)]
    pub account: String,
}

impl Sizes {
    fn overlong(&self) -> Option<&'static str> {
        if self.email.trim().len() > 254 {
            return Some("that email address is too long");
        }
        if self.domain.trim().len() > 253 {
            return Some("that domain is too long");
        }
        if self.front_plan.trim().len() > 64 || self.main_plan.trim().len() > 64 {
            return Some("which size? that plan is not one sold here");
        }
        if !(self.order.is_null() || self.order.is_object()) || self.order.to_string().len() > ORDER_MAX {
            return Some("which order? an order is a short set of named values");
        }
        None
    }

    fn sizes(&self) -> pb::EstateSizes {
        let order = if self.order.is_null() { String::new() } else { self.order.to_string() };
        pb::EstateSizes { front_plan: self.front_plan.trim().to_owned(), main_plan: self.main_plan.trim().to_owned(), main_data_gib: self.main_data_gib, order }
    }
}

/// What a visitor is told when they order a private instance: it is bought by an account here.
pub fn sign_in_first(word: &str, host: &str) -> String {
    format!("A private {word} is bought by your account at {host}, where you see and grow what you bought. Sign in, or Create your account at {host} first.")
}

fn host_of(origin: &str) -> String {
    origin.split_once("://").map(|(_, rest)| rest.split(['/', ':']).next().unwrap_or_default().to_owned()).unwrap_or_default()
}

pub(crate) fn from_status(s: tonic::Status) -> Answer {
    let code = rules::http_status_of(s.code());
    if code == StatusCode::BAD_GATEWAY || code == StatusCode::SERVICE_UNAVAILABLE {
        tracing::warn!(target: AUDIT_TARGET, event = "private.monetize_failed", code = ?s.code(), message = %s.message(), "monetize did not finish a private-instance call");
    }
    Answer::refuse(rules::http_status_of(s.code()), &rules::buyer_words(&s))
}

async fn private_door<S: BuySite>(r: &Relay<S>, ask: &Ask, key: &str) -> Result<(), Answer> {
    if !r.site.allowed("private", &ask.ip, key) {
        tracing::warn!(target: AUDIT_TARGET, event = "private.rate_limited", ip = %ask.ip, "a private-instance request was rate-limited");
        return Err(Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many requests from here; wait a moment"));
    }
    if !r.shop.sells_estates().await {
        return Err(Answer::refuse(StatusCode::NOT_FOUND, &format!("A private {} is not sold here.", r.site.product_word())));
    }
    Ok(())
}

/// **The signed-in ACCOUNT, proven**: the person's own account and its `estate` ticket. A
/// member is refused; nobody signed in is asked to sign in or make the account first.
async fn account_ticket<S: BuySite>(r: &Relay<S>, ask: &Ask) -> Result<(String, Vec<u8>), Answer> {
    let buyer = match r.site.who(&ask.headers).await.map_err(Answer::of)? {
        Who::Person(b) => b,
        Who::Visitor | Who::Machine => return Err(Answer::refuse(StatusCode::UNAUTHORIZED, &sign_in_first(&r.site.product_word(), &host_of(&r.site.origin())))),
    };
    if let Some(m) = &buyer.member {
        let buys = if m.admin.is_empty() { &m.account } else { &m.admin };
        return Err(Answer::refuse(StatusCode::FORBIDDEN, &format!("{} Members don’t buy: {buys} buys for {}.", m.line, m.account)));
    }
    let ticket = r.site.ticket(&ask.headers, &buyer, PURPOSE_ESTATE, &BTreeMap::new()).await.map_err(Answer::of)?;
    Ok((buyer.account, ticket))
}

fn company_of(q: Option<&str>) -> Option<String> {
    let c = q.map(|c| c.trim().to_ascii_lowercase()).unwrap_or_default();
    (!c.is_empty() && c.len() <= 16 && c.chars().all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())).then_some(c)
}

/// `GET /auth/private/ladder[?company=]` → monetize's ladder, as is. For an instance its
/// account's `estate` ticket rides along.
pub async fn private_ladder<S: BuySite>(r: &Relay<S>, ask: &Ask, company: Option<&str>) -> Answer {
    // A new instance's ladder is the site's, kept (LADDER-1/2): answered at once with its age,
    // never a wait on the ONE box's pricing; the sells word is asked only while nothing is kept.
    if company.is_none() {
        if !r.site.allowed("private", &ask.ip, "ladder") {
            return Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many requests from here; wait a moment");
        }
        if let Some(kept) = r.shop.caches().ladders().kept(crate::ladders::PRIVATE) {
            return kept;
        }
        // Nothing kept: the sells word and the first read together, each bounded, so a stuck ONE
        // box costs the page about a second and a shop that sells none still says so at once.
        let (sells, kept) = tokio::join!(tokio::time::timeout(crate::ladders::FIRST_WAIT, r.shop.sells_estates()), r.shop.caches().ladders().answer(&r.shop, crate::ladders::PRIVATE));
        if matches!(sells, Ok(false)) {
            return Answer::refuse(StatusCode::NOT_FOUND, &format!("A private {} is not sold here.", r.site.product_word()));
        }
        return kept;
    }
    if let Err(a) = private_door(r, ask, "ladder").await {
        return a;
    }
    let (company, actor_ticket) = match company_of(company) {
        None if company.is_some() => return Answer::refuse(StatusCode::BAD_REQUEST, "which instance? ?company=<name>"),
        None => (String::new(), Vec::new()),
        Some(co) => match account_ticket(r, ask).await {
            Ok((_, t)) => (co, t),
            Err(a) => return a,
        },
    };
    match r.shop.estate_order().ladder(pb::EstateOrderLadderRequest { company, actor_ticket }).await {
        Ok(l) => Answer::ok(ladder_body(l.into_inner())),
        Err(s) => from_status(s),
    }
}

/// The ladder page's body of an `EstateOrder.Ladder` answer.
pub(crate) fn ladder_body(l: pb::EstateOrderLadderResponse) -> Value {
    json!({
        "steps": l.steps.iter().map(|s| {
            let sizes = s.sizes.clone().unwrap_or_default();
            json!({
                "name": s.name,
                "sizes": { "front_plan": sizes.front_plan, "main_plan": sizes.main_plan, "main_data_gib": sizes.main_data_gib, "order": serde_json::from_str::<serde_json::Value>(&sizes.order).unwrap_or(serde_json::Value::Null) },
                "twin_plan": s.twin_plan,
                "machines": s.machines.iter().map(|m| json!({"role": m.role, "plan": m.plan, "cores": m.cores, "memory_mib": m.memory_mib, "disks_gib": m.disks_gib})).collect::<Vec<_>>(),
                "resources_words": s.resources_words, "monthly_words": s.monthly_words, "total_words": s.total_words,
                "pay_words": s.pay_words, "available": s.available, "reason": s.reason,
            })
        }).collect::<Vec<_>>(),
        "start": l.start, "floor": l.floor, "laid": l.laid, "current": l.current, "apex": l.apex, "term_months": l.term_months,
    })
}


/// `POST /auth/private/start` → `{url, state, words, price_words}`. A visitor pays ONCE (the
/// order names the new account and its e-mail; monetize makes it when paid); signed in, the
/// order is the account's, proven by its `estate` ticket, and no e-mail is sent.
pub async fn private_start<S: BuySite>(r: &Relay<S>, ask: &Ask, body: &Sizes) -> Answer {
    let company = body.company.trim().to_ascii_lowercase();
    if !rules::company(&company) {
        return Answer::refuse(StatusCode::BAD_REQUEST, "which instance? a company name: lowercase letters and digits, a letter first, at most 16");
    }
    if let Some(why) = body.overlong() {
        return Answer::refuse(StatusCode::BAD_REQUEST, why);
    }
    if let Err(a) = private_door(r, ask, &format!("start:{company}")).await {
        return a;
    }
    let origin = r.site.origin();
    let pages = r.site.pages();
    let visitor = matches!(r.site.who(&ask.headers).await, Ok(Who::Visitor));
    let asked = if visitor {
        let account = body.account.trim().to_ascii_lowercase();
        if account.is_empty() || account.len() > 32 {
            return Answer::refuse(StatusCode::BAD_REQUEST, "which account? the name your new account is to have");
        }
        if body.email.trim().is_empty() {
            return Answer::refuse(StatusCode::BAD_REQUEST, "the e-mail address your new account's sign-in link goes to");
        }
        pb::EstateOrderStartRequest {
            company: company.clone(),
            email: body.email.trim().to_owned(),
            domain: body.domain.trim().to_owned(),
            sizes: Some(body.sizes()),
            account,
            actor_ticket: Vec::new(),
            new_account: true,
            welcome_url: urls::private_welcome(&origin, &pages, &company),
            cancel_url: urls::private_cancel(&origin, &pages),
        }
    } else {
        let (account, ticket) = match account_ticket(r, ask).await {
            Ok(t) => t,
            Err(a) => return a,
        };
        // The e-mail is asked once, at account creation, never in a buy (owner 2026-10-22): the
        // order mails the account's own address (monetize `account_email`), whatever a page sent.
        pb::EstateOrderStartRequest {
            company: company.clone(),
            email: String::new(),
            domain: body.domain.trim().to_owned(),
            sizes: Some(body.sizes()),
            account,
            actor_ticket: ticket,
            cancel_url: urls::private_cancel(&origin, &pages),
            ..Default::default()
        }
    };
    let mut client = r.shop.estate_order_minting();
    match tokio::time::timeout(r.shop.deadlines().mint, client.start(asked)).await {
        Err(_) => Answer::refuse(StatusCode::GATEWAY_TIMEOUT, START_TOO_SLOW),
        Ok(Err(s)) => from_status(s),
        Ok(Ok(a)) => {
            let a = a.into_inner();
            if rules::https_url(&a.checkout_url).is_none() {
                return Answer::refuse(StatusCode::BAD_GATEWAY, rules::PAY_URL_REFUSED);
            }
            tracing::info!(target: AUDIT_TARGET, event = "private.start", company = %company, "a private-instance hold was asked for");
            Answer::ok(json!({ "url": a.checkout_url, "state": a.state, "words": a.words, "price_words": a.price_words }))
        }
    }
}

/// `POST /auth/private/change` → `{url, status, words, price_words, reference}`: grow the
/// account's own instance by a step of the ladder.
pub async fn private_change<S: BuySite>(r: &Relay<S>, ask: &Ask, body: &Sizes) -> Answer {
    let company = body.company.trim().to_ascii_lowercase();
    if !rules::company(&company) {
        return Answer::refuse(StatusCode::BAD_REQUEST, "which instance? a company name");
    }
    if let Some(why) = body.overlong() {
        return Answer::refuse(StatusCode::BAD_REQUEST, why);
    }
    if let Err(a) = private_door(r, ask, &format!("change:{company}")).await {
        return a;
    }
    let (_, actor_ticket) = match account_ticket(r, ask).await {
        Ok(t) => t,
        Err(a) => return a,
    };
    // A grow mints a vendor page: the minting channel, and the buyer told at its deadline.
    let mut client = r.shop.estate_order_minting();
    match tokio::time::timeout(r.shop.deadlines().mint, client.change_plan(pb::EstateOrderChangePlanRequest { company: company.clone(), sizes: Some(body.sizes()), actor_ticket, test_order: false })).await {
        Err(_) => Answer::refuse(StatusCode::GATEWAY_TIMEOUT, START_TOO_SLOW),
        Ok(Err(s)) => from_status(s),
        Ok(Ok(c)) => {
            let c = c.into_inner();
            let Some(url) = rules::checked_pay_url(c.checkout_url) else {
                return Answer::refuse(StatusCode::BAD_GATEWAY, rules::PAY_URL_REFUSED);
            };
            tracing::info!(target: AUDIT_TARGET, event = "private.change", company = %company, reference = %c.reference, "an instance's change was asked for");
            Answer::ok(json!({ "url": url, "status": c.status, "words": c.words, "price_words": c.price_words, "reference": c.reference }))
        }
    }
}

/// `GET /auth/private/status?company=` → `{state, words, url, paid_until, records,
/// change_status, change_words}`: the whole order for its account, the state word for anyone
/// else.
pub async fn private_status<S: BuySite>(r: &Relay<S>, ask: &Ask, company: &str) -> Answer {
    let company = company.trim().to_ascii_lowercase();
    if !rules::company(&company) {
        return Answer::refuse(StatusCode::BAD_REQUEST, "which instance? ?company=<name>");
    }
    if !r.site.allowed("private", &ask.ip, &format!("status:{company}")) {
        return Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many requests from here; wait a moment");
    }
    let actor_ticket = match account_ticket(r, ask).await {
        Ok((_, t)) => t,
        Err(a) if a.status.is_server_error() => return a,
        Err(_) => Vec::new(),
    };
    match r.shop.estate_order().status(pb::EstateOrderStatusRequest { company, actor_ticket }).await {
        Ok(s) => {
            let s = s.into_inner();
            Answer::ok(json!({
                "state": s.state, "words": s.words, "url": s.url, "paid_until": s.paid_until,
                "records": s.dns_records.iter().map(|d| json!({"name": d.name, "kind": d.kind, "value": d.value})).collect::<Vec<_>>(),
                "change_status": s.change_status, "change_words": s.change_words,
            }))
        }
        Err(s) => from_status(s),
    }
}

/// What a door naming an instance is given.
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct CompanyBody {
    #[serde(default)]
    pub company: String,
}

/// `POST /auth/private/sign-in-again` → `{words}`: the ready instance's first sign-in link,
/// mailed again to the order's address.
pub async fn private_sign_in_again<S: BuySite>(r: &Relay<S>, ask: &Ask, body: &CompanyBody) -> Answer {
    let company = body.company.trim().to_ascii_lowercase();
    if !rules::company(&company) {
        return Answer::refuse(StatusCode::BAD_REQUEST, "which instance? a company name");
    }
    if !r.site.allowed("private", &ask.ip, &format!("sign-in-again:{company}")) {
        return Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many requests from here; wait a moment");
    }
    let (_, actor_ticket) = match account_ticket(r, ask).await {
        Ok(t) => t,
        Err(a) => return a,
    };
    match r.shop.estate_order().send_sign_in_again(pb::EstateOrderSignInAgainRequest { company: company.clone(), actor_ticket }).await {
        Ok(a) => {
            tracing::info!(target: AUDIT_TARGET, event = "private.sign_in_again", company = %company, "a new sign-in link was asked for by its account");
            Answer::ok(json!({ "words": a.into_inner().words }))
        }
        Err(s) => from_status(s),
    }
}

// ── managing an instance (Gunnar's `auth::private` renew / cancel / terminate / restore-front) ──

/// The budget a managing door spends (Gunnar's `door`: offered here and inside budget; these
/// doors act on an instance already sold, so the shop's estates offer is not asked).
fn manage_budget<S: BuySite>(r: &Relay<S>, ask: &Ask, key: &str) -> Result<(), Answer> {
    if !r.site.allowed("private", &ask.ip, key) {
        tracing::warn!(target: AUDIT_TARGET, event = "private.rate_limited", ip = %ask.ip, "a private-instance request was rate-limited");
        return Err(Answer::refuse(StatusCode::TOO_MANY_REQUESTS, "too many requests from here; wait a moment"));
    }
    Ok(())
}

fn company_named(body: &CompanyBody) -> Result<String, Answer> {
    let company = body.company.trim().to_ascii_lowercase();
    if rules::company(&company) {
        Ok(company)
    } else {
        Err(Answer::refuse(StatusCode::BAD_REQUEST, "which instance? a company name"))
    }
}

/// `POST /auth/private/renew {company}` → `{url, words, price_words, paid_until}`: **Renew <co>**,
/// the signed-in account's own instance by its `estate` ticket; monetize prices it and answers
/// the payment page.
pub async fn private_renew<S: BuySite>(r: &Relay<S>, ask: &Ask, body: &CompanyBody) -> Answer {
    let company = match company_named(body) {
        Ok(c) => c,
        Err(a) => return a,
    };
    if let Err(a) = manage_budget(r, ask, &format!("renew:{company}")) {
        return a;
    }
    let (_, actor_ticket) = match account_ticket(r, ask).await {
        Ok(t) => t,
        Err(a) => return a,
    };
    // A renewal mints a vendor page: the minting channel, and the buyer told at its deadline.
    let mut client = r.shop.estate_order_minting();
    match tokio::time::timeout(r.shop.deadlines().mint, client.renew(pb::EstateOrderRenewRequest { company: company.clone(), actor_ticket })).await {
        Err(_) => Answer::refuse(StatusCode::GATEWAY_TIMEOUT, START_TOO_SLOW),
        Ok(Err(s)) => from_status(s),
        Ok(Ok(a)) => {
            let a = a.into_inner();
            if rules::https_url(&a.checkout_url).is_none() {
                return Answer::refuse(StatusCode::BAD_GATEWAY, rules::PAY_URL_REFUSED);
            }
            tracing::info!(target: AUDIT_TARGET, event = "private.renew", company = %company, reference = %a.reference, "an instance's renewal was asked for");
            Answer::ok(json!({ "url": a.checkout_url, "words": a.words, "price_words": a.price_words, "paid_until": a.paid_until }))
        }
    }
}

/// `POST /auth/private/cancel {company}` → `{words, paid_until, delete_on}`: **Stop renewing**:
/// the instance runs to the end of its paid period, is then stopped with its data kept, and
/// deleted after the grace.
pub async fn private_stop_renewing<S: BuySite>(r: &Relay<S>, ask: &Ask, body: &CompanyBody) -> Answer {
    let company = match company_named(body) {
        Ok(c) => c,
        Err(a) => return a,
    };
    if let Err(a) = manage_budget(r, ask, &format!("cancel:{company}")) {
        return a;
    }
    let (_, actor_ticket) = match account_ticket(r, ask).await {
        Ok(t) => t,
        Err(a) => return a,
    };
    match r.shop.estate_order().cancel(pb::EstateOrderCancelRequest { company: company.clone(), actor_ticket }).await {
        Err(s) => from_status(s),
        Ok(c) => {
            let c = c.into_inner();
            tracing::info!(target: AUDIT_TARGET, event = "private.cancel", company = %company, "an instance's renewal was stopped by its account");
            Answer::ok(json!({ "words": c.words, "paid_until": c.paid_until, "delete_on": c.delete_on }))
        }
    }
}

/// `POST /auth/private/restore-front {company}` → `{words, moved, public_ipv4}`: **a lost front
/// made again**; monetize restores it from its daily backup (or lays it again and says so).
pub async fn private_restore_front<S: BuySite>(r: &Relay<S>, ask: &Ask, body: &CompanyBody) -> Answer {
    let company = match company_named(body) {
        Ok(c) => c,
        Err(a) => return a,
    };
    if let Err(a) = manage_budget(r, ask, &format!("restore-front:{company}")) {
        return a;
    }
    let (_, actor_ticket) = match account_ticket(r, ask).await {
        Ok(t) => t,
        Err(a) => return a,
    };
    match r.shop.estate_order().restore_front(pb::EstateOrderRestoreFrontRequest { company: company.clone(), actor_ticket }).await {
        Err(s) => from_status(s),
        Ok(f) => {
            let f = f.into_inner();
            tracing::info!(target: AUDIT_TARGET, event = "private.restore_front", company = %company, moved = f.moved, "an instance's lost front was restored by its account");
            Answer::ok(json!({ "words": f.words, "moved": f.moved, "public_ipv4": f.public_ipv4 }))
        }
    }
}

/// What `POST /auth/private/terminate` is given: which instance and the name typed back. No
/// backup is offered, ever (A32-27): an older page's `backup` word is not read.
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct TerminateBody {
    #[serde(default)]
    pub company: String,
    #[serde(default)]
    pub confirm: String,
}

/// Pure: **the name typed back names the instance** — its company, or a host under it.
/// monetize checks it again, exactly; this only keeps a slip from dialling anything.
pub fn typed_back(company: &str, typed: &str) -> bool {
    let typed = typed.trim().to_ascii_lowercase();
    !typed.is_empty() && (typed == company || typed.starts_with(&format!("{company}.")))
}

/// `POST /auth/private/terminate {company, confirm}` → `{words, done}`: **Terminate <co>**. The
/// account's own `estate` ticket, a recent user-verified login ([`BuySite::stepped_up`]), the
/// name typed back; no backup is taken.
pub async fn private_terminate<S: BuySite>(r: &Relay<S>, ask: &Ask, body: &TerminateBody) -> Answer {
    let company = body.company.trim().to_ascii_lowercase();
    if !rules::company(&company) {
        return Answer::refuse(StatusCode::BAD_REQUEST, "which instance? a company name");
    }
    if !typed_back(&company, &body.confirm) {
        return Answer::refuse(StatusCode::BAD_REQUEST, &format!("Type the instance's name, {company}, to terminate it. Nothing was ended."));
    }
    if let Err(a) = manage_budget(r, ask, &format!("terminate:{company}")) {
        return a;
    }
    // A recent fresh login: a person whose user-verified login is older than the site's window
    // logs in again first. Nobody signed in is told so by `account_ticket`.
    if let Ok(Who::Person(_)) = r.site.who(&ask.headers).await {
        if let Err(why) = r.site.stepped_up(&ask.headers).await {
            let words = format!("{why} Nothing was ended.");
            return Answer { status: StatusCode::FORBIDDEN, body: json!({ "error": words, "code": STEP_UP_CODE, "message": words }), see_other: None };
        }
    }
    let (_, actor_ticket) = match account_ticket(r, ask).await {
        Ok(t) => t,
        Err(a) => return a,
    };
    match r.shop.estate_order().terminate(pb::EstateOrderTerminateRequest { company: company.clone(), actor_ticket, confirm: body.confirm.trim().to_owned() }).await {
        Err(s) => from_status(s),
        Ok(t) => {
            let t = t.into_inner();
            tracing::warn!(target: AUDIT_TARGET, event = "private.terminate", company = %company, done = t.done, "an instance's terminate was asked for by its account");
            Answer::ok(json!({ "words": t.words, "done": t.done }))
        }
    }
}

/// `GET /paid/estate/{company}` → 303 to the order's watch on the site.
pub fn paid_estate<S: BuySite>(r: &Relay<S>, company: &str) -> Answer {
    Answer::see_other(urls::paid_estate_to(&r.site.pages(), company))
}

/// `GET /paid/estate/{company}/{change}` → 303 to the order's watch, told it is a change.
pub fn paid_estate_change<S: BuySite>(r: &Relay<S>, company: &str, change: &str) -> Answer {
    Answer::see_other(urls::paid_change_to(&r.site.pages(), company, change))
}

// ── the account's grow (Gunnar's `api::subscription` bundles / buy-bundle / status) ──

async fn person<S: BuySite>(r: &Relay<S>, ask: &Ask) -> Result<Buyer, Answer> {
    match r.site.who(&ask.headers).await.map_err(Answer::of)? {
        Who::Person(b) => match &b.member {
            Some(m) => Err(Answer::refuse(StatusCode::FORBIDDEN, &member_buys_nothing(m))),
            None => Ok(b),
        },
        _ => Err(Answer::refuse(StatusCode::UNAUTHORIZED, "Sign in to the account that buys first.")),
    }
}

fn refused_call(s: tonic::Status) -> Answer {
    Answer::refuse(rules::http_status_of(s.code()), &rules::buyer_words(&s))
}

fn json_of<T: serde::Serialize>(v: &T) -> Answer {
    Answer::ok(serde_json::to_value(v).unwrap_or(Value::Null))
}

async fn listing<S: BuySite>(r: &Relay<S>, ask: &Ask, buyer: &Buyer, market: pb::Market) -> Result<pb::ListBundlesResponse, Answer> {
    let ticket = r.site.ticket(&ask.headers, buyer, PURPOSE_READ, &BTreeMap::new()).await.map_err(Answer::of)?;
    r.shop
        .purchase()
        .bundles(pb::ListBundlesRequest { product: r.shop.product().to_owned(), tenant: buyer.tenant().to_owned(), market: market as i32, actor_ticket: ticket })
        .await
        .map(tonic::Response::into_inner)
        .map_err(refused_call)
}

/// `GET /api/account/subscription/bundles` → [`Window`](crate::purchase::Window): "Buy a plan"
/// (no plan held) or "Grow your account", only what the box can serve.
pub async fn buy_bundles<S: BuySite>(r: &Relay<S>, ask: &Ask) -> Answer {
    let market = match rules::market_for(None, ask.accept_language()) {
        Ok(m) => m,
        Err(why) => return Answer::refuse(StatusCode::BAD_REQUEST, &why),
    };
    let buyer = match person(r, ask).await {
        Ok(b) => b,
        Err(a) => return a,
    };
    if let Err(a) = grow_offered(r).await {
        return a;
    }
    match listing(r, ask, &buyer, market).await {
        Ok(w) => json_of(&crate::purchase::Window { market: rules::market_word(market as i32).to_owned(), ..crate::purchase::window_of(w) }),
        Err(a) => a,
    }
}

/// What `POST /api/account/subscription/buy-bundle` is given: one opaque id.
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct BundleOrder {
    pub bundle: String,
}

/// `POST /api/account/subscription/buy-bundle {bundle}` → [`Attempt`](crate::purchase::Attempt):
/// "Upgrade". The delta is monetize's window's, the held caps the product's, and the ticket
/// covers exactly the caps the order resolves to.
pub async fn buy_bundle<S: BuySite>(r: &Relay<S>, ask: &Ask, order: &BundleOrder) -> Answer {
    let market = match rules::market_for(None, ask.accept_language()) {
        Ok(m) => m,
        Err(why) => return Answer::refuse(StatusCode::BAD_REQUEST, &why),
    };
    let buyer = match person(r, ask).await {
        Ok(b) => b,
        Err(a) => return a,
    };
    if let Err(a) = grow_offered(r).await {
        return a;
    }
    let held = match r.site.held_caps(&ask.headers, &buyer).await {
        Ok(h) => h,
        Err(why) => return Answer::of(why),
    };
    let window = match listing(r, ask, &buyer, market).await {
        Ok(w) => w,
        Err(a) => return a,
    };
    let Some(chosen) = window.bundles.into_iter().find(|b| b.id == order.bundle) else {
        return Answer::refuse(StatusCode::BAD_REQUEST, "that is not something this account can order right now. Reload the page for what is available.");
    };
    let caps = crate::purchase::order_caps(&chosen, &held);
    let ticket = if crate::purchase::is_renewal(&chosen) {
        r.site.ticket(&ask.headers, &buyer, PURPOSE_RENEW, &BTreeMap::new()).await
    } else {
        r.site.ticket(&ask.headers, &buyer, PURPOSE_ORDER, &caps).await
    };
    let ticket = match ticket {
        Ok(t) => t,
        Err(why) => return Answer::of(why),
    };
    let mut client = r.shop.purchase_minting();
    let asked = client.start(pb::StartPurchaseRequest {
        product: r.shop.product().to_owned(),
        tenant: buyer.tenant().to_owned(),
        caps: Default::default(),
        vendor: String::new(),
        payer: buyer.payer.clone(),
        starts_unix_ms: 0,
        market: market as i32,
        renew: false,
        bundle: order.bundle.clone(),
        actor_ticket: ticket,
        users: None,
    });
    let started = match tokio::time::timeout(r.shop.deadlines().mint + crate::link::MINT_GRACE, asked).await {
        Err(_) => return Answer::refuse(StatusCode::GATEWAY_TIMEOUT, START_TOO_SLOW),
        Ok(Err(s)) => return refused_call(s),
        Ok(Ok(a)) => a.into_inner(),
    };
    let Some(url) = rules::checked_pay_url(started.redirect_url.clone()) else {
        return Answer::refuse(StatusCode::BAD_GATEWAY, rules::PAY_URL_REFUSED);
    };
    json_of(&crate::purchase::bundle_attempt(started, &chosen, &held, &caps, url))
}

/// `GET /api/account/subscription/status?reference=` → [`Attempt`](crate::purchase::Attempt):
/// the way back from the vendor (`/paid/{reference}`, `/cancel/{reference}`), scoped by
/// monetize to the buyer's tenant.
pub async fn buy_status<S: BuySite>(r: &Relay<S>, ask: &Ask, reference: &str) -> Answer {
    let buyer = match person(r, ask).await {
        Ok(b) => b,
        Err(a) => return a,
    };
    if let Err(a) = grow_offered(r).await {
        return a;
    }
    let ticket = match r.site.ticket(&ask.headers, &buyer, PURPOSE_READ, &BTreeMap::new()).await {
        Ok(t) => t,
        Err(why) => return Answer::of(why),
    };
    match r.shop.purchase().status(pb::PurchaseStatusRequest { reference: reference.to_owned(), product: r.shop.product().to_owned(), tenant: buyer.tenant().to_owned(), actor_ticket: ticket }).await {
        Ok(p) => {
            let p = p.into_inner();
            let Some(url) = rules::checked_pay_url(p.redirect_url.clone()) else {
                return Answer::refuse(StatusCode::BAD_GATEWAY, rules::PAY_URL_REFUSED);
            };
            json_of(&crate::purchase::status_attempt(p, url))
        }
        Err(s) => refused_call(s),
    }
}

// ── the account's own renewal and history (Gunnar's `api::subscription` offer / renew / history) ──

/// What a buyer reads where the shop does not verify the product's renewal tickets: buyer
/// words only (BUYFLOW-4; the reason, that the shop cannot check who renews, is the operator's).
pub const RENEWAL_NOT_VERIFIED: &str = "This account cannot be renewed here right now. Nothing has been charged — please contact us and we will set it right.";

/// **Whether the shop verifies this product's tenant renewal tickets** (`Products.List`
/// `tenant_renewal`; Gunnar's `offered`): a renewal door is not there on a shop that cannot
/// check the ticket — the same answer as a site with no monetize at all (501). Asked after who
/// asks: a member, or nobody signed in, is told that first.
async fn offered<S: BuySite>(r: &Relay<S>) -> Result<(), Answer> {
    // A shop with no `Products` service cannot say it verifies them: it does not (Gunnar's
    // `offered`), the same 501 as a shop that says no.
    let answer = match r.shop.products().list(pb::ListProductsRequest {}).await {
        Ok(a) => a.into_inner(),
        Err(s) if s.code() == tonic::Code::Unimplemented => return Err(Answer::refuse(StatusCode::NOT_IMPLEMENTED, RENEWAL_NOT_VERIFIED)),
        Err(s) => return Err(refused_call(s)),
    };
    if answer.products.iter().any(|p| p.id == r.shop.product() && p.tenant_renewal) {
        Ok(())
    } else {
        Err(Answer::refuse(StatusCode::NOT_IMPLEMENTED, RENEWAL_NOT_VERIFIED))
    }
}

/// **The grow doors' gate** (A32-9: every site, no knob): [`offered`] — a grow door needs a
/// checked, unspent ONE box ticket (owner decision 2026-10-26, item 1).
async fn grow_offered<S: BuySite>(r: &Relay<S>) -> Result<(), Answer> {
    offered(r).await
}

/// `GET /api/account/subscription` → [`Renewal`](crate::purchase::Renewal): what another period
/// would come to. Quotes and moves nothing; the `renew` ticket rides on the quote too, so a
/// person who may not renew is told so before a price. A refusal is an answer, not a failure.
pub async fn account_offer<S: BuySite>(r: &Relay<S>, ask: &Ask) -> Answer {
    let buyer = match person(r, ask).await {
        Ok(b) => b,
        Err(a) => return a,
    };
    if let Err(a) = offered(r).await {
        return a;
    }
    let ticket = match r.site.ticket(&ask.headers, &buyer, PURPOSE_RENEW, &BTreeMap::new()).await {
        Ok(t) => t,
        Err(why) => return Answer::of(why),
    };
    let quote = r
        .shop
        .purchase()
        .quote(pb::QuoteRequest { product: r.shop.product().to_owned(), tenant: buyer.tenant().to_owned(), caps: Default::default(), starts_unix_ms: 0, market: pb::Market::Unspecified as i32, renew: true, bundle: String::new(), actor_ticket: ticket })
        .await;
    match quote {
        Ok(q) => json_of(&crate::purchase::renewal_of(q.into_inner())),
        Err(s) if s.code() == tonic::Code::FailedPrecondition => {
            let nothing = matches!(r.site.held_caps(&ask.headers, &buyer).await, Ok(held) if held.is_empty());
            json_of(&crate::purchase::refused_renewal(s.message(), rules::customer_words(&s), nothing))
        }
        Err(s) => refused_call(s),
    }
}

/// `POST /api/account/subscription/renew` → [`Attempt`](crate::purchase::Attempt): **Prolong**,
/// another period of what is held. No vendor named: monetize picks the one serving the market.
pub async fn account_renew<S: BuySite>(r: &Relay<S>, ask: &Ask) -> Answer {
    let buyer = match person(r, ask).await {
        Ok(b) => b,
        Err(a) => return a,
    };
    if let Err(a) = offered(r).await {
        return a;
    }
    let ticket = match r.site.ticket(&ask.headers, &buyer, PURPOSE_RENEW, &BTreeMap::new()).await {
        Ok(t) => t,
        Err(why) => return Answer::of(why),
    };
    let asked = pb::StartPurchaseRequest { product: r.shop.product().to_owned(), tenant: buyer.tenant().to_owned(), caps: Default::default(), vendor: String::new(), payer: buyer.payer.clone(), starts_unix_ms: 0, market: pb::Market::Unspecified as i32, renew: true, bundle: String::new(), actor_ticket: ticket, users: None };
    let mut client = r.shop.purchase_minting();
    let started = match tokio::time::timeout(r.shop.deadlines().mint, client.start(asked)).await {
        Err(_) => return Answer::refuse(StatusCode::GATEWAY_TIMEOUT, START_TOO_SLOW),
        Ok(Ok(s)) => s.into_inner(),
        Ok(Err(s)) => return refused_call(s),
    };
    let Some(url) = rules::checked_pay_url(started.redirect_url.clone()) else {
        return Answer::refuse(StatusCode::BAD_GATEWAY, rules::PAY_URL_REFUSED);
    };
    tracing::info!(target: AUDIT_TARGET, event = "subscription.renewed", by = "tenant", who = %buyer.account, namespace = %buyer.tenant(), reference = %started.reference, state = crate::purchase::state_name(started.state), "a tenant renewed their own subscription");
    json_of(&crate::purchase::renew_attempt(started, url))
}

/// `GET /api/account/subscription/history` → `[Past]`: what this account has bought. A `read`
/// ticket: a read buys nothing, and must not spend the one the person came to renew with.
pub async fn account_history<S: BuySite>(r: &Relay<S>, ask: &Ask) -> Answer {
    let buyer = match person(r, ask).await {
        Ok(b) => b,
        Err(a) => return a,
    };
    if let Err(a) = offered(r).await {
        return a;
    }
    let ticket = match r.site.ticket(&ask.headers, &buyer, PURPOSE_READ, &BTreeMap::new()).await {
        Ok(t) => t,
        Err(why) => return Answer::of(why),
    };
    match r.shop.purchase().history(pb::PurchaseHistoryRequest { product: r.shop.product().to_owned(), tenant: buyer.tenant().to_owned(), actor_ticket: ticket, limit: 0 }).await {
        Ok(h) => json_of(&h.into_inner().attempts.into_iter().map(crate::purchase::past_of).collect::<Vec<_>>()),
        Err(s) => refused_call(s),
    }
}

/// The ledger's reason for a buyer's own cancel (Gunnar's console's, word for word).
pub const CANCELLED_BY_CUSTOMER: &str = "the customer cancelled the order before paying";

/// What `POST /api/account/subscription/cancel` is given: the order's reference.
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct Following {
    pub reference: String,
}

/// `POST /api/account/subscription/cancel {reference}` → [`Attempt`](crate::purchase::Attempt):
/// **the buyer's "Cancel order"**, for an order not yet paid (Gunnar's
/// `api::subscription::cancel`, moved as it is). monetize's `Purchase.Cancel`, scoped by
/// monetize: the tenant is the buyer's own, the product's `cancel` ticket (covering no amount)
/// vouches for it, and monetize decides — an order with nothing captured is closed (any vendor
/// hold released), a captured one is refused, another tenant's reference is NOT_FOUND.
pub async fn buy_cancel<S: BuySite>(r: &Relay<S>, ask: &Ask, which: &Following) -> Answer {
    let buyer = match person(r, ask).await {
        Ok(b) => b,
        Err(a) => return a,
    };
    if let Err(a) = grow_offered(r).await {
        return a;
    }
    let ticket = match r.site.ticket(&ask.headers, &buyer, PURPOSE_CANCEL, &BTreeMap::new()).await {
        Ok(t) => t,
        Err(why) => return Answer::of(why),
    };
    let ask_shop = pb::CancelPurchaseRequest { product: r.shop.product().to_owned(), tenant: buyer.tenant().to_owned(), actor_ticket: ticket, reference: which.reference.clone(), reason: CANCELLED_BY_CUSTOMER.to_owned() };
    match r.shop.purchase().cancel(ask_shop).await {
        Ok(closed) => {
            let closed = closed.into_inner();
            tracing::info!(target: AUDIT_TARGET, event = "subscription.cancelled", by = "tenant", who = %buyer.account, namespace = %buyer.tenant(), reference = %which.reference, already_closed = closed.already_closed, "a tenant cancelled an order they had not paid");
            json_of(&crate::purchase::cancelled_attempt(closed))
        }
        Err(s) => refused_call(s),
    }
}

/// `GET /api/account/purchases` → [`Purchases`](crate::purchase::Purchases): the account's own,
/// asked at monetize with its `estate` ticket.
pub async fn buy_purchases<S: BuySite>(r: &Relay<S>, ask: &Ask) -> Answer {
    let buyer = match person(r, ask).await {
        Ok(b) => b,
        Err(a) => return a,
    };
    let ticket = match r.site.ticket(&ask.headers, &buyer, PURPOSE_ESTATE, &BTreeMap::new()).await {
        Ok(t) => t,
        Err(why) => return Answer::of(why),
    };
    let answer = match r.shop.estate_order().purchases(pb::EstateOrderPurchasesRequest { account: buyer.tenant().to_owned(), actor_ticket: ticket }).await {
        Ok(a) => a.into_inner(),
        // A shop that sells no instances, or predates the read: nothing it can say.
        Err(s) if s.code() == tonic::Code::Unimplemented => pb::EstateOrderPurchasesResponse::default(),
        Err(s) => return refused_call(s),
    };
    let offer = if crate::purchase::without_room(&answer) { r.shop.account_offer().await } else { None };
    json_of(&crate::purchase::purchases_of(answer, &r.site.pages(), &r.site.product_word(), &host_of(&r.site.origin()), offer))
}

/// `GET /auth/buy/offer` → `{signed_in, account, member, account_url, account_offer, private_offered,
/// private_url}`: what a site's pages draw before anything is chosen (Gunnar's `/auth/status`
/// shop fields). The links are the site's own pages.
pub async fn buy_offer<S: BuySite>(r: &Relay<S>, ask: &Ask) -> Answer {
    let who = match r.site.who(&ask.headers).await {
        Ok(w) => w,
        Err(why) => return Answer::of(why),
    };
    let private = r.shop.sells_estates().await;
    let pages = r.site.pages();
    // What an account is, as monetize sells it here ("5GB"): the visitor's account pill
    // (Gunnar's `/auth/status` `account_offer`). Absent signed in, and where none is sold.
    let account_offer = match &who {
        Who::Visitor => r.shop.account_offer().await,
        _ => None,
    };
    let (signed_in, account, member) = match &who {
        Who::Person(b) => (true, b.account.clone(), b.member.as_ref().map(|m| m.line.clone())),
        _ => (false, String::new(), None),
    };
    let mut offer = json!({
        "signed_in": signed_in,
        "account": account,
        "member": member,
        "account_url": pages.account,
        "private_offered": private,
        "private_url": if private { pages.private } else { String::new() },
    });
    // Only where one is sold (absent otherwise, as Gunnar's `/auth/status` leaves it).
    if let Some(a) = account_offer {
        offer["account_offer"] = json!(a);
    }
    Answer::ok(offer)
}

// ── the router ─────────────────────────────────────────────────────────────────────────

/// **The client's address** (Gunnar's console's rule, `security::client_ip_from`): the
/// right-most `X-Forwarded-For` hop that no trusted proxy wrote, and only when the socket's
/// `peer` is loopback or one of `trusted`; otherwise the peer itself; `unknown` without a peer
/// (one shared bucket: there is nothing to judge the header by). Each proxy appends the
/// address it saw, so only the right of the chain is vouched for; a visitor wrote the left.
pub fn client_ip(headers: &HeaderMap, peer: Option<IpAddr>, trusted: &[IpAddr]) -> String {
    let believable = peer.is_some_and(|p| p.is_loopback() || trusted.contains(&p));
    let forwarded = believable
        .then(|| {
            let hops: Vec<IpAddr> = headers.get_all("x-forwarded-for").iter().filter_map(|v| v.to_str().ok()).flat_map(|v| v.split(',')).filter_map(|hop| hop.trim().parse().ok()).collect();
            hops.into_iter().rev().find(|hop| !hop.is_loopback() && !trusted.contains(hop))
        })
        .flatten();
    match forwarded.or(peer) {
        Some(ip) => ip.to_string(),
        None => "unknown".to_owned(),
    }
}

type Shared<S> = State<Arc<Relay<S>>>;

/// The doors read the client from the first `X-Forwarded-For` hop, which [`router`]'s
/// [`one_client_hop`] layer has already set to [`client_ip`]'s answer.
fn ask_of(headers: HeaderMap) -> Ask {
    let ip = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()).unwrap_or("unknown").to_owned();
    Ask { headers, ip }
}

/// The router's first step: decide the client once, from the socket (`ConnectInfo`) and the
/// site's [`BuySite::trusted_proxies`], and leave exactly that one hop in `X-Forwarded-For`
/// (what a trusted proxy in front would write). `X-Real-IP` is dropped: no door believes it.
async fn one_client_hop<S: BuySite>(State(r): Shared<S>, mut req: axum::extract::Request, next: axum::middleware::Next) -> Response {
    let peer = req.extensions().get::<axum::extract::ConnectInfo<SocketAddr>>().map(|c| c.0.ip());
    let ip = client_ip(req.headers(), peer, r.site.trusted_proxies());
    let headers = req.headers_mut();
    headers.remove("x-real-ip");
    headers.remove("x-forwarded-for");
    if let Ok(v) = ip.parse() {
        headers.insert("x-forwarded-for", v);
    }
    next.run(req).await
}

// ── "Add users" (Gunnar's account admin; A32-25, moved from gunnar-ui-server `api::subscription`) ──

/// What `POST /api/account/users` is given: the account the users join, how many, the room
/// each brings (GiB), and for ONE named user its name and the address its invite goes to.
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct UsersOrder {
    #[serde(default)]
    pub account: String,
    #[serde(default)]
    pub count: u32,
    #[serde(default)]
    pub room_gib: u64,
    #[serde(default)]
    pub name: String,
    #[serde(default)]
    pub email: String,
}

/// Pure: **what an "Add users" ticket covers**, exactly monetize's `UsersAsk::ticket_caps`:
/// the users and the room they bring.
pub fn users_caps(count: u32, room_bytes: u64) -> BTreeMap<String, u64> {
    BTreeMap::from([("users".to_owned(), u64::from(count)), ("user_room_bytes".to_owned(), room_bytes)])
}

/// `POST /api/account/users {account, count, room_gib, name, email}`: **"Add users"** where the
/// site sells room. ONE monetize call, `Purchase.Start` with `users`, under an ORDER ticket the
/// product mints for exactly these users at this room ([`users_caps`]) on the account named,
/// so the PRODUCT decides who may (Gunnar mints it for the account's admin only). monetize asks
/// the product whether the users fit in the account's unused room and grants them at once if
/// they do; otherwise it sells exactly the rest through ONE checkout, or refuses naming what is
/// missing. The named user's invite is redeemed at the site's own origin. The answer is the
/// console's (`state`, `reference`, `checkout_url`, `url`, `words`, `users`, `principal`, the
/// seats and room, `mailed`); a refusal is `{code, message}`.
/// **How many users "Add users" buys** (RA4-21): the count asked; none (or 0) is a refusal
/// by name, never one user bought for a malformed ask.
pub fn users_asked(count: u32) -> Result<u32, &'static str> {
    if count == 0 {
        return Err("say how many users");
    }
    Ok(count)
}

pub async fn add_users<S: BuySite>(r: &Relay<S>, ask: &Ask, order: &UsersOrder) -> Answer {
    let account = order.account.trim();
    if account.is_empty() {
        return operator_refusal(StatusCode::BAD_REQUEST, "invalidargument", "name the account the users join");
    }
    // A member of the account may well be its admin: whether this person may add its users is
    // the product's ticket, not the relay's guess.
    let buyer = match r.site.who(&ask.headers).await {
        Ok(Who::Person(b)) => Buyer { tenant: account.to_owned(), ..b },
        Ok(_) => return operator_refusal(StatusCode::UNAUTHORIZED, "unauthenticated", "Sign in to the account first."),
        Err(why) => return operator_refusal(why.status, "unavailable", &why.words),
    };
    // After who asks (a stranger is told to sign in first, whatever the count).
    let count = match users_asked(order.count) {
        Ok(n) => n,
        Err(words) => return operator_refusal(StatusCode::BAD_REQUEST, "invalidargument", words),
    };
    let users = pb::UsersOrder { count, room_bytes: order.room_gib.saturating_mul(1 << 30), name: order.name.trim().to_owned(), email: order.email.trim().to_owned(), invite_origin: r.site.origin() };
    let caps = users_caps(users.count, users.room_bytes);
    let ticket = match r.site.ticket(&ask.headers, &buyer, PURPOSE_ORDER, &caps).await {
        Ok(t) => t,
        Err(why) => {
            let code = if why.status == StatusCode::FORBIDDEN { "permissiondenied" } else if why.status == StatusCode::UNAUTHORIZED { "unauthenticated" } else { "unavailable" };
            return operator_refusal(why.status, code, &why.words);
        }
    };
    let mut client = r.shop.purchase_minting();
    let asked = client.start(pb::StartPurchaseRequest {
        product: r.shop.product().to_owned(),
        tenant: account.to_owned(),
        market: pb::Market::Unspecified as i32,
        users: Some(users),
        actor_ticket: ticket,
        ..Default::default()
    });
    let started = match tokio::time::timeout(r.shop.deadlines().mint + crate::link::MINT_GRACE, asked).await {
        Err(_) => return operator_refusal(StatusCode::GATEWAY_TIMEOUT, "deadlineexceeded", START_TOO_SLOW),
        Ok(Err(s)) => return operator_refused(s),
        Ok(Ok(a)) => a.into_inner(),
    };
    let checkout = match operator_pay_url(started.redirect_url.clone()) {
        Ok(u) => u,
        Err(a) => return a,
    };
    let grant = started.users.clone();
    let granted = grant.as_ref().is_some_and(|u| u.granted);
    tracing::info!(target: AUDIT_TARGET, event = "account.users_ordered", account, reference = %started.reference, state = crate::purchase::state_name(started.state), "an account's users were ordered");
    Answer::ok(json!({
        "state": if granted { "added" } else { crate::purchase::state_name(started.state) },
        "reference": started.reference,
        "checkout_url": checkout,
        "url": grant.as_ref().map(|u| u.invite_url.clone()).unwrap_or_default(),
        "words": started.words,
        "users": grant.clone().map(crate::purchase::UsersLine::from),
        "principal": grant.as_ref().filter(|u| u.granted && !u.principal.is_empty()).map(|u| json!({ "id": u.principal, "name": u.principal })),
        "seats": grant.as_ref().map_or(0, |u| u.seats),
        "seats_open": grant.as_ref().map_or(0, |u| u.seats_open),
        "room_bytes": grant.as_ref().map_or(0, |u| u.room_bytes),
        "invite": "",
        "mailed": grant.as_ref().map(|u| u.invited.clone()).unwrap_or_default(),
    }))
}

// ── an operator's orders for a tenant (Gunnar's console's `api::increase`) ────────────────

/// **An operator door's refusal**, in the console's `{code, message}` as well as `error`: the
/// code is the shop's own (`failedprecondition`), the words the shop's, verbatim (an operator
/// reads them; a buyer's doors put [`words_for_a_visitor`] there instead).
pub fn operator_refusal(status: StatusCode, code: &str, words: &str) -> Answer {
    Answer { status, body: json!({ "error": words, "code": code, "message": words }), see_other: None }
}

/// Pure: **the HTTP status and code word of a refused shop call on an operator door**
/// (Gunnar's console's `grpc::Fail::http_status`, for the codes a shop answers).
pub fn operator_status_of(code: tonic::Code) -> (StatusCode, String) {
    let word = format!("{code:?}").to_lowercase();
    let status = match code {
        tonic::Code::NotFound => StatusCode::NOT_FOUND,
        tonic::Code::AlreadyExists => StatusCode::CONFLICT,
        tonic::Code::InvalidArgument | tonic::Code::FailedPrecondition | tonic::Code::OutOfRange => StatusCode::BAD_REQUEST,
        tonic::Code::Unimplemented => StatusCode::NOT_IMPLEMENTED,
        tonic::Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
        tonic::Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
        tonic::Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
        tonic::Code::PermissionDenied => StatusCode::FORBIDDEN,
        _ => StatusCode::BAD_GATEWAY,
    };
    (status, word)
}

fn operator_refused(s: tonic::Status) -> Answer {
    let (status, code) = operator_status_of(s.code());
    operator_refusal(status, &code, s.message())
}

/// The vendor's link as a browser may be given it, or the console's 502 (`badpayurl`).
fn operator_pay_url(raw: String) -> Result<String, Answer> {
    rules::checked_pay_url(raw).ok_or_else(|| {
        tracing::error!(target: AUDIT_TARGET, event = "purchase.bad_pay_url", "monetize answered a checkout link that is not https");
        operator_refusal(StatusCode::BAD_GATEWAY, "badpayurl", rules::PAY_URL_REFUSED)
    })
}

/// **Who orders, for which tenant**: the site's operator ([`BuySite::operator`]) and the tenant
/// in the PATH, never one in a body. Refused before the shop is dialled.
async fn operator_for<S: BuySite>(r: &Relay<S>, ask: &Ask, tenant: &str) -> Result<crate::site::Operator, Answer> {
    let op = r.site.operator(&ask.headers).await.map_err(|why| {
        let code = match why.status {
            StatusCode::FORBIDDEN => "permissiondenied",
            StatusCode::UNAUTHORIZED => "unauthenticated",
            _ => "",
        };
        operator_refusal(why.status, code, &why.words)
    })?;
    if tenant.is_empty() {
        return Err(operator_refusal(StatusCode::BAD_REQUEST, "invalidargument", "an order needs a namespace"));
    }
    Ok(op)
}

fn operator_market(ask: &Ask, named: Option<&str>) -> Result<pb::Market, Answer> {
    rules::market_for(named, ask.accept_language()).map_err(|why| operator_refusal(StatusCode::BAD_REQUEST, "invalidargument", &why))
}

/// What the operator's typed order and offer are given: one meter and the cap to END UP with
/// (a target, as monetize's order message is), and the market (absent: the browser's, else
/// monetize's default).
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct TypedOrder {
    pub meter: String,
    pub cap: u64,
    #[serde(default)]
    pub market: Option<String>,
}

/// What the operator's window order is given: one opaque id, and the market.
#[derive(Clone, Debug, Default, serde::Deserialize)]
pub struct OperatorBundleOrder {
    pub bundle: String,
    #[serde(default)]
    pub market: Option<String>,
}

/// `GET /api/namespaces/{name}/increase` → [`Purchasable`](crate::purchase::Purchasable): what
/// this product sells more of. A failure to ASK is answered with its sentence, not propagated:
/// the door is there, so the deployment sells, and the reader is owed why there is nothing.
pub async fn operator_purchasable<S: BuySite>(r: &Relay<S>) -> Answer {
    use crate::purchase::{sellable, Purchasable};
    let answer = match r.shop.products().list(pb::ListProductsRequest {}).await {
        Ok(a) => a.into_inner(),
        Err(e) => {
            return json_of(&Purchasable { meters: Vec::new(), unavailable: Some(format!("this console could not reach the billing service to ask what is for sale: {}", e.message())) });
        }
    };
    let meters = sellable(&answer, r.shop.product());
    let unavailable = meters.is_empty().then(|| "the billing service answered, and lists nothing priced and enforced for this product right now".to_owned());
    json_of(&Purchasable { meters, unavailable })
}

/// `GET /api/namespaces/{name}/increase/offer?meter=&cap=` → [`Offer`](crate::purchase::Offer):
/// an order priced, not placed. Never a renewal, no bundle, and NO ticket: the operator path.
pub async fn operator_offer<S: BuySite>(r: &Relay<S>, ask: &Ask, tenant: &str, order: &TypedOrder) -> Answer {
    if let Err(a) = operator_for(r, ask, tenant).await {
        return a;
    }
    let mut client = r.shop.purchase();
    let quote = client.quote(pb::QuoteRequest {
        product: r.shop.product().to_owned(),
        tenant: tenant.to_owned(),
        caps: [(order.meter.clone(), order.cap)].into_iter().collect(),
        market: pb::Market::Unspecified as i32,
        starts_unix_ms: 0,
        renew: false,
        bundle: String::new(),
        actor_ticket: Vec::new(),
    });
    match quote.await {
        Ok(q) => json_of(&crate::purchase::render_offer(q.into_inner())),
        Err(s) => operator_refused(s),
    }
}

/// One operator order: `caps` named (typed) or `bundle` (the window), never both.
async fn operator_start<S: BuySite>(r: &Relay<S>, ask: &Ask, tenant: &str, market: Option<&str>, caps: BTreeMap<String, u64>, bundle: &str) -> Answer {
    let op = match operator_for(r, ask, tenant).await {
        Ok(op) => op,
        Err(a) => return a,
    };
    let market = match operator_market(ask, market) {
        Ok(m) => m,
        Err(a) => return a,
    };
    let mut client = r.shop.purchase_minting();
    let asked = client.start(pb::StartPurchaseRequest {
        product: r.shop.product().to_owned(),
        tenant: tenant.to_owned(),
        caps: caps.into_iter().collect(),
        // Empty on purpose: monetize answers the free provider at price 0, else the one vendor
        // serving the market; a site guessing between two would be choosing who gets paid.
        vendor: String::new(),
        payer: op.payer.clone(),
        starts_unix_ms: 0,
        market: market as i32,
        renew: false,
        bundle: bundle.to_owned(),
        actor_ticket: Vec::new(),
        users: None,
    });
    let started = match asked.await {
        Ok(a) => a.into_inner(),
        Err(s) => return operator_refused(s),
    };
    let state = crate::purchase::state_name(started.state);
    tracing::info!(target: AUDIT_TARGET, event = "increase.started", who = %op.who, tenant = %tenant, bundle = %bundle, reference = %started.reference, state, "an operator placed an order for a tenant");
    let url = match operator_pay_url(started.redirect_url) {
        Ok(u) => u,
        Err(a) => return a,
    };
    json_of(&crate::purchase::Placed { reference: started.reference, state, url, reason: started.reason })
}

/// `POST /api/namespaces/{name}/increase {meter, cap, market}` → [`Placed`](crate::purchase::Placed):
/// the operator's typed order (the target cap), placed.
pub async fn operator_order<S: BuySite>(r: &Relay<S>, ask: &Ask, tenant: &str, order: &TypedOrder) -> Answer {
    operator_start(r, ask, tenant, order.market.as_deref(), [(order.meter.clone(), order.cap)].into(), "").await
}

/// `POST /api/namespaces/{name}/increase/bundle {bundle, market}` → [`Placed`](crate::purchase::Placed):
/// one from the window, by its opaque id and nothing else (no caps: monetize refuses both).
pub async fn operator_bundle<S: BuySite>(r: &Relay<S>, ask: &Ask, tenant: &str, order: &OperatorBundleOrder) -> Answer {
    operator_start(r, ask, tenant, order.market.as_deref(), BTreeMap::new(), &order.bundle).await
}

/// `GET /api/namespaces/{name}/increase/bundles[?market=]` → [`Window`](crate::purchase::Window):
/// what this deployment sells the tenant, priced, only what it can serve. No ticket.
pub async fn operator_bundles<S: BuySite>(r: &Relay<S>, ask: &Ask, tenant: &str, market: Option<&str>) -> Answer {
    let market = match operator_market(ask, market) {
        Ok(m) => m,
        Err(a) => return a,
    };
    if let Err(a) = operator_for(r, ask, tenant).await {
        return a;
    }
    match r.shop.purchase().bundles(pb::ListBundlesRequest { product: r.shop.product().to_owned(), tenant: tenant.to_owned(), market: market as i32, actor_ticket: Vec::new() }).await {
        Ok(w) => json_of(&crate::purchase::Window { market: rules::market_word(market as i32).to_owned(), ..crate::purchase::window_of(w.into_inner()) }),
        Err(s) => operator_refused(s),
    }
}

/// `GET /api/namespaces/{name}/increase/status?reference=` → [`Placed`](crate::purchase::Placed):
/// an order followed, scoped by monetize to this product and the path's tenant (one uniform
/// NOT_FOUND otherwise). The outcome is the ledger's, never a return URL's.
pub async fn operator_status<S: BuySite>(r: &Relay<S>, ask: &Ask, tenant: &str, reference: &str) -> Answer {
    if let Err(a) = operator_for(r, ask, tenant).await {
        return a;
    }
    let progress = match r.shop.purchase().status(pb::PurchaseStatusRequest { reference: reference.to_owned(), product: r.shop.product().to_owned(), tenant: tenant.to_owned(), actor_ticket: Vec::new() }).await {
        Ok(p) => p.into_inner(),
        Err(s) => return operator_refused(s),
    };
    let url = match operator_pay_url(progress.redirect_url) {
        Ok(u) => u,
        Err(a) => return a,
    };
    json_of(&crate::purchase::Placed { reference: progress.reference, state: crate::purchase::state_name(progress.state), url, reason: progress.reason })
}

/// **Which groups of doors a site mounts** ([`router_of`]). A site that sells nothing to start
/// (a sold Gunnar) still reads an account's own; a site that sells no accounts has no sign-up.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Doors {
    /// `/auth/signup/*`: a new account, its name, its status, its link again.
    pub signup: bool,
    /// `/auth/private/*` and `/paid/estate/*`: a private instance, ordered and managed.
    pub private: bool,
    /// The account's own reads and its cancel: `/api/account/subscription` (the quote),
    /// `…/status`, `…/cancel`, `…/bundles`, `…/history` and `/api/account/purchases`.
    pub account: bool,
    /// The doors that START an order: `…/buy-bundle` and `…/renew`.
    pub orders: bool,
    /// `/auth/buy/offer`: what the pages draw before anything is chosen.
    pub offer: bool,
    /// **An operator's reads for a tenant named in the path** (Gunnar's console):
    /// `GET /api/namespaces/{name}/increase`, `…/offer`, `…/status`, `…/bundles`. Never in
    /// [`Doors::ALL`]: a site names it, and says who may ([`BuySite::operator`]).
    pub operator: bool,
    /// **The operator's two that START an order**: `POST /api/namespaces/{name}/increase` (typed)
    /// and `…/increase/bundle` (from the window). Mounted with [`Doors::operator`] only.
    pub operator_orders: bool,
    /// **"Add users"** (Gunnar's account admin, A32-25): `POST /api/account/users`
    /// ([`add_users`]). Never in [`Doors::ALL`]: a site that sells room for users names it.
    pub users: bool,
}

impl Doors {
    /// Every buyer's door ([`router`]); no operator's (a site names those itself).
    pub const ALL: Doors = Doors { signup: true, private: true, account: true, orders: true, offer: true, operator: false, operator_orders: false, users: false };
    /// None (a site adds the groups it sells: `Doors { private: true, ..Doors::NONE }`).
    pub const NONE: Doors = Doors { signup: false, private: false, account: false, orders: false, offer: false, operator: false, operator_orders: false, users: false };
}

/// **Every door, mounted**: the routes of the module's table, for a site that has no router of
/// its own for them (Holger's front). Merge it in front of the site's own pages.
pub fn router<S: BuySite>(relay: Arc<Relay<S>>) -> Router {
    router_of::<S, ()>(relay, Doors::ALL)
}

/// **The doors of the groups `doors` names, mounted** (Gunnar's console: the sign-up and
/// private doors where it sells, the account's reads where it has a monetize, the orders where
/// its buyers buy). A door of a group not named is not there at all: no route, axum's 404.
///
/// The answer is a `Router<T>` for the site's own router state `T`, so a site whose router
/// holds a state of its own (Gunnar's `AppState`) merges it before its layers and
/// `with_state`, and its own middleware (sign-in, CSRF, role floors) runs in front of the doors.
pub fn router_of<S: BuySite, T: Clone + Send + Sync + 'static>(relay: Arc<Relay<S>>, doors: Doors) -> Router<T> {
    // The plans pages' ladders are read as soon as the doors exist (LADDER-2): the first view
    // after a front starts finds them kept, never a ONE box pricing them for seconds.
    let mut warm = Vec::new();
    if doors.signup {
        warm.push(crate::ladders::signup_key(pb::Market::Unspecified as i32));
    }
    if doors.private {
        warm.push(crate::ladders::PRIVATE.to_string());
    }
    if !warm.is_empty() {
        relay.shop.caches().ladders().warm(&relay.shop, &warm);
    }
    let mut r: Router<Arc<Relay<S>>> = Router::new();
    if doors.signup {
        r = r
            .route("/auth/signup/ladder", get(|State(r): Shared<S>, h: HeaderMap, Query(q): Query<HashMap<String, String>>| async move { signup_plans(&r, &ask_of(h), q.get("market").map(String::as_str)).await }))
            .route("/auth/signup/name", get(|State(r): Shared<S>, h: HeaderMap, Query(q): Query<HashMap<String, String>>| async move { signup_name(&r, &ask_of(h), q.get("u").map(String::as_str).unwrap_or("")).await }))
            .route("/auth/signup/start", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<SignupStart>| async move { signup_start(&r, &ask_of(h), &b).await }))
            .route("/auth/signup/status", get(|State(r): Shared<S>, h: HeaderMap, Query(q): Query<HashMap<String, String>>| async move { signup_status(&r, &ask_of(h), q.get("session").map(String::as_str).unwrap_or("")).await }))
            .route("/auth/signup/rename", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<SignupRename>| async move { signup_rename(&r, &ask_of(h), &b).await }))
            .route("/auth/signup/sign-in-again", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<SignInAgain>| async move { signup_sign_in_again(&r, &ask_of(h), &b).await }));
    }
    if doors.private {
        r = r
            .route("/auth/private/ladder", get(|State(r): Shared<S>, h: HeaderMap, Query(q): Query<HashMap<String, String>>| async move { private_ladder(&r, &ask_of(h), q.get("company").map(String::as_str)).await }))
            .route("/auth/private/start", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<Sizes>| async move { private_start(&r, &ask_of(h), &b).await }))
            .route("/auth/private/change", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<Sizes>| async move { private_change(&r, &ask_of(h), &b).await }))
            .route("/auth/private/status", get(|State(r): Shared<S>, h: HeaderMap, Query(q): Query<HashMap<String, String>>| async move { private_status(&r, &ask_of(h), q.get("company").map(String::as_str).unwrap_or("")).await }))
            .route("/auth/private/renew", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<CompanyBody>| async move { private_renew(&r, &ask_of(h), &b).await }))
            .route("/auth/private/cancel", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<CompanyBody>| async move { private_stop_renewing(&r, &ask_of(h), &b).await }))
            .route("/auth/private/terminate", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<TerminateBody>| async move { private_terminate(&r, &ask_of(h), &b).await }))
            .route("/auth/private/restore-front", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<CompanyBody>| async move { private_restore_front(&r, &ask_of(h), &b).await }))
            .route("/auth/private/sign-in-again", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<CompanyBody>| async move { private_sign_in_again(&r, &ask_of(h), &b).await }))
            .route("/paid/estate/{company}", get(|State(r): Shared<S>, Path(co): Path<String>| async move { paid_estate(&r, &co) }))
            .route("/paid/estate/{company}/{change}", get(|State(r): Shared<S>, Path((co, ch)): Path<(String, String)>| async move { paid_estate_change(&r, &co, &ch) }));
    }
    if doors.account {
        r = r
            .route("/api/account/purchases", get(|State(r): Shared<S>, h: HeaderMap| async move { buy_purchases(&r, &ask_of(h)).await }))
            .route("/api/account/subscription/bundles", get(|State(r): Shared<S>, h: HeaderMap| async move { buy_bundles(&r, &ask_of(h)).await }))
            .route("/api/account/subscription", get(|State(r): Shared<S>, h: HeaderMap| async move { account_offer(&r, &ask_of(h)).await }))
            .route("/api/account/subscription/history", get(|State(r): Shared<S>, h: HeaderMap| async move { account_history(&r, &ask_of(h)).await }))
            .route("/api/account/subscription/cancel", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<Following>| async move { buy_cancel(&r, &ask_of(h), &b).await }))
            .route("/api/account/subscription/status", get(|State(r): Shared<S>, h: HeaderMap, Query(q): Query<HashMap<String, String>>| async move { buy_status(&r, &ask_of(h), q.get("reference").map(String::as_str).unwrap_or("")).await }));
    }
    if doors.orders {
        r = r
            .route("/api/account/subscription/buy-bundle", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<BundleOrder>| async move { buy_bundle(&r, &ask_of(h), &b).await }))
            .route("/api/account/subscription/renew", post(|State(r): Shared<S>, h: HeaderMap| async move { account_renew(&r, &ask_of(h)).await }));
    }
    if doors.offer {
        r = r.route("/auth/buy/offer", get(|State(r): Shared<S>, h: HeaderMap| async move { buy_offer(&r, &ask_of(h)).await }));
    }
    if doors.users {
        r = r.route("/api/account/users", post(|State(r): Shared<S>, h: HeaderMap, Json(b): Json<UsersOrder>| async move { add_users(&r, &ask_of(h), &b).await }));
    }
    if doors.operator {
        let purchasable = get(|State(r): Shared<S>| async move { operator_purchasable(&r).await });
        let purchasable = if doors.operator_orders { purchasable.post(|State(r): Shared<S>, h: HeaderMap, Path(t): Path<String>, Json(b): Json<TypedOrder>| async move { operator_order(&r, &ask_of(h), &t, &b).await }) } else { purchasable };
        r = r
            .route("/api/namespaces/{name}/increase", purchasable)
            .route("/api/namespaces/{name}/increase/offer", get(|State(r): Shared<S>, h: HeaderMap, Path(t): Path<String>, Query(q): Query<TypedOrder>| async move { operator_offer(&r, &ask_of(h), &t, &q).await }))
            .route("/api/namespaces/{name}/increase/status", get(|State(r): Shared<S>, h: HeaderMap, Path(t): Path<String>, Query(q): Query<HashMap<String, String>>| async move { operator_status(&r, &ask_of(h), &t, q.get("reference").map(String::as_str).unwrap_or("")).await }))
            .route("/api/namespaces/{name}/increase/bundles", get(|State(r): Shared<S>, h: HeaderMap, Path(t): Path<String>, Query(q): Query<HashMap<String, String>>| async move { operator_bundles(&r, &ask_of(h), &t, q.get("market").map(String::as_str)).await }));
        if doors.operator_orders {
            r = r.route("/api/namespaces/{name}/increase/bundle", post(|State(r): Shared<S>, h: HeaderMap, Path(t): Path<String>, Json(b): Json<OperatorBundleOrder>| async move { operator_bundle(&r, &ask_of(h), &t, &b).await }));
        }
    }
    r.layer(axum::extract::DefaultBodyLimit::max(BUY_BODY_MAX)).layer(axum::middleware::from_fn_with_state(relay.clone(), one_client_hop::<S>)).with_state(relay)
}

/// **The most a buy door reads** (P5-SEC): every POST body here is a few named values (an
/// order is at most [`ORDER_MAX`]), so anything over this is refused 413 before it is parsed,
/// whatever the site's own router allows. axum's 2 MiB default would otherwise be the bound.
/// Room enough that a field over its own rule (100 KB of company name) is still judged and
/// answered 400 by name, as a site's console tests it (gunnar-ui's private start).
pub const BUY_BODY_MAX: usize = 256 * 1024;

/// The paths [`router`] answers, for a site that routes by path before it reaches the relay.
pub fn is_relay_path(path: &str) -> bool {
    path.starts_with("/auth/signup/") || path.starts_with("/auth/private/") || path == "/auth/buy/offer" || path == "/api/account/subscription" || path.starts_with("/api/account/subscription/") || path == "/api/account/purchases" || path == "/api/account/users" || path.starts_with("/paid/estate/")
}

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

    /// ★ RA4-21 RED: **"Add users" with no count (or 0) is refused by name**, never one user
    /// bought for a malformed ask.
    #[test]
    fn add_users_with_no_count_is_refused_by_name() {
        assert_eq!(users_asked(0), Err("say how many users"));
        assert_eq!(users_asked(3), Ok(3));
    }

    #[test]
    fn the_relay_answers_only_its_own_paths() {
        assert!(is_relay_path("/auth/signup/start") && is_relay_path("/paid/estate/acme") && is_relay_path("/api/account/subscription/status") && is_relay_path("/api/account/users"));
        assert!(!is_relay_path("/paid/purchase-1") && !is_relay_path("/-/signup") && !is_relay_path("/auth/passkey/login/start"));
    }

    #[test]
    fn the_client_is_the_peer_unless_a_trusted_proxy_vouches() {
        let ip = |s: &str| s.parse::<IpAddr>().unwrap();
        let mut h = HeaderMap::new();
        h.insert("x-forwarded-for", "203.0.113.9, 10.0.0.1".parse().unwrap());
        // An untrusted peer: the header is ignored.
        assert_eq!(client_ip(&h, Some(ip("198.51.100.4")), &[]), "198.51.100.4");
        // Loopback: the right-most hop no trusted proxy wrote.
        assert_eq!(client_ip(&h, Some(ip("127.0.0.1")), &[]), "10.0.0.1");
        // A named proxy, and its own hop skipped.
        assert_eq!(client_ip(&h, Some(ip("10.0.0.2")), &[ip("10.0.0.2"), ip("10.0.0.1")]), "203.0.113.9");
        // Not an address: skipped; nothing left: the peer.
        h.insert("x-forwarded-for", "nonsense, 127.0.0.1".parse().unwrap());
        assert_eq!(client_ip(&h, Some(ip("::1")), &[]), "::1");
        assert_eq!(client_ip(&h, None, &[]), "unknown");
    }

    #[test]
    fn every_group_is_named_and_none_is_none() {
        assert_eq!(Doors::ALL, Doors { signup: true, private: true, account: true, orders: true, offer: true, operator: false, operator_orders: false, users: false });
        assert_eq!(Doors::NONE, Doors { signup: false, private: false, account: false, orders: false, offer: false, operator: false, operator_orders: false, users: false });
        assert_eq!(Doors { private: true, ..Doors::NONE }, Doors { signup: false, private: true, account: false, orders: false, offer: false, operator: false, operator_orders: false, users: false });
    }

    #[test]
    fn an_operator_door_reads_the_shops_code_and_words() {
        assert_eq!(operator_status_of(tonic::Code::FailedPrecondition), (StatusCode::BAD_REQUEST, "failedprecondition".to_owned()));
        assert_eq!(operator_status_of(tonic::Code::ResourceExhausted), (StatusCode::TOO_MANY_REQUESTS, "resourceexhausted".to_owned()));
        assert_eq!(operator_status_of(tonic::Code::Internal), (StatusCode::BAD_GATEWAY, "internal".to_owned()));
        let a = operator_refused(tonic::Status::not_found("no such order for acme"));
        assert_eq!((a.status, a.body), (StatusCode::NOT_FOUND, json!({"error": "no such order for acme", "code": "notfound", "message": "no such order for acme"})));
    }

    #[test]
    fn a_refusal_is_the_pages_error_shape() {
        let a = Answer::refuse(StatusCode::CONFLICT, "username \"x14\" is taken; choose another");
        assert_eq!(a.body, json!({"error": "username \"x14\" is taken; choose another"}));
    }
}