crates/monetize-server/src/estate_order/account.rs
//! **The account that buys and owns a private Gunnar is PROVEN** (one shop,
//! owner 2026-10-07).
//!
//! A customer is a gunnar.rs ACCOUNT. The shop's own console asks as the
//! signed-in person: it has the shop's OWN Gunnar mint an `estate` actor
//! ticket ([`monetize_embed::PURPOSE_ESTATE`], `Billing.MintActorTicket`) for
//! that account — a member of an account is refused one there — and hands
//! the bytes over with the call. The host checks it ([`crate::site_ticket::may_act`],
//! A32-13): under the appliance key the shop holds for its own Gunnar, for that
//! account, for that verb, fresh, and unspent (the host's replay ring). So:
//!
//! * `EstateOrder.Start` holds for the account the ticket proves — the run's
//! `account` is no longer a typed string;
//! * `EstateOrder.Ladder{company}` and `EstateOrder.ChangePlan` answer only
//! the run's own account ([`owner_of`]), in ONE sentence whatever the cause,
//! so no estate is told apart from another by its refusal.
//!
//! A SOLD estate's own Gunnar proves nothing here: the shop does not hold its
//! key. (The separate owner's door keyed to a sold estate's appliance key,
//! private ownership (c), is gone.)
use crate::estate_order::EstateRun;
use crate::state::AppState;
/// The one answer when the account is not proven, the product called as `w` says.
pub fn not_proven(w: &super::lifecycle::ProductWords) -> String {
let home = if w.account_home.is_empty() {
String::new()
} else {
format!("{} ", w.account_home)
};
format!("a private {} is bought, read and changed by its {home}account: sign in to that account and ask again", w.title)
}
/// Why an account was not proven.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unproven {
/// The ticket does not prove the account: [`NOT_PROVEN`] to the caller.
Refused,
/// The shop's Gunnar could not be asked; ask again.
Unavailable(String),
}
/// **`account`'s `estate` ticket, checked** — from `site` (the calling estate, ST-3) under
/// that site's own main's key ([`crate::site_ticket::may_act`]) for the product that site
/// sells; with no site, by the shop's gunnar product as before. `account` is the account's
/// name on its site (plain, or `@<site>/<name>`). What went wrong goes to the operator's log
/// (never the ticket).
pub fn proven(
state: &AppState,
site: Option<&str>,
account: &str,
ticket: &[u8],
) -> Result<(), Unproven> {
let refused = |why: &str| {
eprintln!("monetize-server: an estate call for account {account:?} was refused: {why}");
Unproven::Refused
};
if account.trim().is_empty() {
return Err(refused("no account"));
}
if ticket.is_empty() {
return Err(refused("no ticket"));
}
let product_id = match site.and_then(|e| state.registry.product_of_estate(e)) {
Some(p) => p,
None => state
.sold_product()
.map_err(Unproven::Unavailable)?
.to_owned(),
};
let Some(product) = state.registry.find_product(&product_id) else {
return Err(Unproven::Unavailable(format!(
"this shop serves no {product_id} product, so it cannot prove an account"
)));
};
let none = std::collections::BTreeMap::new();
match crate::site_ticket::may_act(
state,
product,
site,
account,
monetize_embed::PURPOSE_ESTATE,
&none,
ticket,
) {
Ok(monetize_product::ActorVerdict::Allowed) => Ok(()),
Ok(monetize_product::ActorVerdict::Refused(why)) => Err(refused(&why)),
Err(monetize_product::ProductError::Unavailable(why)) => Err(Unproven::Unavailable(why)),
Err(monetize_product::ProductError::Refused(why)) => Err(refused(&why)),
}
}
/// **The account's name on its own site**: `account` as given, or `@<site>/<name>` of the
/// calling site spelt in full; another site's account is `Err` (refused, one sentence).
pub fn plain_account(site: Option<&str>, account: &str) -> Result<String, Unproven> {
match (
site.map(str::trim).filter(|e| !e.is_empty()),
monetize_product::TenantId(account.to_owned()).estate(),
) {
(_, None) => Ok(account.to_owned()),
(Some(site), Some((e, name))) if e == site => Ok(name.to_owned()),
_ => Err(Unproven::Refused),
}
}
/// One room line of an account (`EstateOrder.Purchases`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Room {
pub state: String,
pub paid_until_ms: Option<u64>,
pub cap_bytes: u64,
}
/// Pure: **`account`'s room here**, from the facts this shop holds — none for
/// an account at the shop without room (its fact is Free with
/// [`crate::signup::NO_ROOM_CAP`]: it holds no room, so there is no line).
pub fn rooms_of(
facts: &[monetize_product::EntitlementFact],
account: &str,
cap_meter: &str,
) -> Vec<Room> {
facts
.iter()
.filter(|f| f.tenant.0 == account)
// An account at the shop without room: its fact holds no room here.
.filter(|f| {
!(f.state == monetize_product::State::Free
&& f.caps.get(cap_meter).copied() == Some(crate::signup::NO_ROOM_CAP))
})
.map(|f| Room {
state: format!("{:?}", f.state).to_ascii_lowercase(),
paid_until_ms: f.paid_until_unix_ms,
cap_bytes: f.caps.get(cap_meter).copied().unwrap_or(0),
})
.collect()
}
/// Pure: **whether `account` is an account at this shop without room here** —
/// it holds the shop's account fact (Free, [`crate::signup::NO_ROOM_CAP`]).
pub fn is_account_here(
facts: &[monetize_product::EntitlementFact],
account: &str,
cap_meter: &str,
) -> bool {
facts.iter().any(|f| {
f.tenant.0 == account
&& f.state == monetize_product::State::Free
&& f.caps.get(cap_meter).copied() == Some(crate::signup::NO_ROOM_CAP)
})
}
/// **The order whose account the ticket proves**: `company`'s run, when the
/// ticket is its account's — on the run's OWN site, under that site's key (ST-3/ST-5), and
/// only for a call from that site; [`Unproven::Refused`] for an order nobody placed too, and
/// for another site's order, so a stranger learns nothing about which estates exist.
pub fn owner_of(
state: &AppState,
site: Option<&str>,
company: &str,
ticket: &[u8],
) -> Result<EstateRun, Unproven> {
let Some(run) = state.estates.get(company) else {
eprintln!("monetize-server: an estate call for {company:?} was refused: no such order");
return Err(Unproven::Refused);
};
let bought_on = crate::estate_order::site_of_run(state, &run);
if let Some(caller) = site.map(str::trim).filter(|e| !e.is_empty()) {
if bought_on.as_deref() != Some(caller) {
eprintln!("monetize-server: an estate call for {company:?} from {caller} was refused: it was bought on {bought_on:?}");
return Err(Unproven::Refused);
}
}
proven(state, bought_on.as_deref(), &run.account, ticket)?;
Ok(run)
}
/// ★ **The account a visitor's order makes once it is PAID** (pay once, owner 2026-10-22): a
/// private estate bought by a visitor with no account here meets the vendor ONCE — the
/// estate's payment — and the account is made at that payment, as a paid signup's is: the
/// signup sink makes it on the order's site (a pulled product's main makes it when it next
/// calls monetize), its record is kept under [`crate::signup::ACCOUNT_VENDOR`] with `session`,
/// and its sign-in link (`welcome_url` with the session) is mailed to `email`.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct NewAccount {
/// The product the account is made for, and its account plan (no price, no room).
pub product: String,
pub plan: String,
pub currency: monetize_pay::Currency,
/// The name the account is to have (the run's `account`) and where its link goes.
pub username: String,
pub email: String,
/// The console's welcome page, `{session}` in it.
pub welcome_url: String,
/// The shop's own session for the account ([`crate::signup::account_session`]): the key
/// of its record, and so what makes the payment's account ONE account.
pub session: String,
/// The estate the order came from (the caller's bearer), and its site's host.
#[serde(default)]
pub estate: Option<String>,
#[serde(default)]
pub site: Option<String>,
/// Where the vendor's page sends the payer back (the ONE shop's own status page, lane
/// G12); empty: the vendor's own return.
#[serde(default)]
pub back_to: String,
/// Where the vendor's page sends a payer who CANCELS (A10 row 6: the site's own
/// `/signup?plan=private`); empty: the way back, [`Self::back_to`].
#[serde(default)]
pub cancel_to: String,
/// ★ A33-2/A33-4: the vendor's TEST payment method that pays the hold server-side (the test
/// button's press, a test key only); empty: the visitor pays at the vendor's page. Data, the
/// only thing a test press's order differs in.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub test_payer: String,
}