nordisk/monetize
crates/monetize-server/src/services/signup.rs
//! **`Signup.Status`** — what became of a vendor checkout session, and
//! **`SignupAdmin.List`** — the operator's read of the same store.
//!
//! # Why `UNKNOWN` is not an error
//!
//! The return page opens the moment the vendor redirects the browser back, and
//! the webhook is a SEPARATE delivery over a different connection. Which of the
//! two arrives first is the vendor's business and it is regularly the page. So
//! a session nobody has heard of answers `UNKNOWN` with an empty everything —
//! not `NotFound`, which a polling page would have to special-case as "keep
//! going" while every other `NotFound` in this API means "stop". The page polls
//! until it sees `CREATED` or `REFUSED`, and `UNKNOWN` and `PENDING` are both
//! "not yet".
//!
//! # The e-mail is on the operator's wire only
//!
//! [`crate::signup::SignupRecord`] keeps the address the buyer typed (and
//! confirmed) before the checkout. `Status` never carries it: it is polled by a
//! page holding nothing but a session id the vendor put in a URL. `List` does,
//! behind the operator bearer — changed 2026-09-15, when the first paid signup
//! showed an operator an account with no way to know whom to write to.
use std::sync::Arc;
use monetize_proto::signup_admin_server::SignupAdmin;
use monetize_proto::signup_server::Signup;
use monetize_proto::{self as pb};
use tonic::{Request, Response, Status};
use super::blocking;
use crate::signup::{SignupRecord, SignupState};
use crate::state::AppState;
use crate::store::StoreError;
pub struct SignupService(pub Arc<AppState>);
pub struct SignupAdminService(pub Arc<AppState>);
fn store_err(e: StoreError) -> Status {
Status::internal(format!("signup store: {e}"))
}
/// The wire state of a record. One writer for both services (one implementation).
fn state_of(rec: &SignupRecord) -> pb::signup_status_response::State {
use pb::signup_status_response::State as S;
match rec.state {
SignupState::Pending => S::Pending,
SignupState::Created { .. } => S::Created,
SignupState::Refused { .. } => S::Refused,
}
}
/// The wire refusal of a record. `REFUSAL_UNSPECIFIED` for anything that is not
/// a refusal at all, which is what proto3's zero value means here.
fn refusal_of(rec: &SignupRecord) -> pb::signup_status_response::Refusal {
use pb::signup_status_response::Refusal as R;
match rec.state {
SignupState::Refused { refusal, .. } => match refusal {
crate::signup::Refusal::Unknown => R::Unspecified,
crate::signup::Refusal::Name => R::Name,
crate::signup::Refusal::Final => R::Final,
},
_ => R::Unspecified,
}
}
/// What the RETURN PAGE is told — a customer's surface, so `reason` and
/// `words` are the customer's sentences (`crate::signup::customer_words`) and
/// never the product's own message. The operator's copy is [`row_to_pb`].
fn status_response(
store: &crate::signup::SignupStore,
rec: &SignupRecord,
now_ms: i64,
) -> pb::SignupStatusResponse {
let (words, reason) = crate::signup::customer_words(store, rec, now_ms);
let (words_if_silent, silent_after_ms) = crate::signup::words_ahead(store, rec, now_ms);
let (username, namespace) = match &rec.state {
SignupState::Pending | SignupState::Refused { .. } => (String::new(), String::new()),
// The username the PRODUCT settled on, not the one the customer typed:
// they are usually the same and the difference is the whole reason
// `SignupOutcome::Created` carries both.
SignupState::Created {
namespace,
username,
..
} => (username.clone(), namespace.clone()),
};
pb::SignupStatusResponse {
state: state_of(rec) as i32,
username,
namespace,
reason,
product: rec.signup.product.clone(),
plan: rec.signup.plan.clone(),
paid_at_unix_ms: rec.signup.paid_at_unix_ms,
refusal: refusal_of(rec) as i32,
words,
words_if_silent,
silent_after_ms,
}
}
/// **A "not now" on a customer's surface**: the real reason to the log and to
/// `functional_status`, the customer's sentence on the wire. `what` names the
/// verb for the operator's line.
fn unavailable_for_customer(what: &str, why: &str, customer: &str) -> Status {
skidbladnir::functional_status(
"monetize-server/signup",
what,
true,
&format!("unavailable: {why}"),
);
eprintln!("monetize-server: signup {what} unavailable: {why}");
Status::unavailable(customer)
}
/// **The plan a `Signup.Start` sells**, and the refusal when that is not one
/// thing.
///
/// Empty means "the only one". A console carries no price list — the whole
/// reason `[signups.<product>.plans]` lives here is that the price is the operator's — and
/// a deployment that sells exactly one plan should not make every caller name
/// it. Two plans and no name is a refusal that LISTS them, never a pick:
/// guessing which of two prices a stranger meant to pay is the one mistake that
/// cannot be undone once the card is charged.
fn plan_for<'a>(
plans: &'a std::collections::BTreeMap<String, crate::config::SignupPlan>,
asked: &str,
) -> Result<(&'a str, crate::config::SignupPlan), Status> {
let known: Vec<&str> = plans.keys().map(String::as_str).collect();
if !asked.is_empty() {
return plans
.get_key_value(asked)
.map(|(k, v)| (k.as_str(), *v))
.ok_or_else(|| {
Status::failed_precondition(format!(
"plan {asked:?} is not sold here; [signups.<product>.plans] names {known:?}"
))
});
}
match known.as_slice() {
[] => Err(Status::unimplemented(
"this deployment sells no signups: [signups.<product>.plans] is empty, so there is nothing a checkout page could charge for",
)),
[only] => Ok((*only, plans[*only])),
_ => Err(Status::failed_precondition(format!(
"this deployment sells {} signup plans and the request named none; ask for one of {known:?}",
known.len()
))),
}
}
/// **[`plan_for`], told the customer's market** (g-currencies, money
/// 2026-09-28): with no plan named and several sold, the plan priced in THAT
/// market's currency — one plan per market is how the Danish company sells a
/// signup to a Swede in SEK, a Finn in EUR and a Dane in DKK. A named plan
/// wins; one plan is the plan; no market is the deployment's DEFAULT market
/// when each market sells exactly one plan (release-3); otherwise (or none
/// priced for it) it falls back to [`plan_for`]'s refusal that lists them.
fn plan_for_market<'a>(
plans: &'a std::collections::BTreeMap<String, crate::config::SignupPlan>,
asked: &str,
market: Option<monetize_pay::Market>,
default_market: monetize_pay::Market,
) -> Result<(&'a str, crate::config::SignupPlan), Status> {
// An account at the shop is taken only when it is NAMED: a request that
// names no plan (the functional test, an older page) is choosing ROOM.
if asked.is_empty() && plans.values().any(crate::signup::is_account_plan) {
let selling: std::collections::BTreeMap<String, crate::config::SignupPlan> = plans
.iter()
.filter(|(_, p)| !crate::signup::is_account_plan(p))
.map(|(k, v)| (k.clone(), *v))
.collect();
return plan_for_market(&selling, asked, market, default_market).map(|(id, p)| {
(
plans
.get_key_value(id)
.map(|(k, _)| k.as_str())
.unwrap_or_default(),
p,
)
});
}
if asked.is_empty() && plans.len() > 1 {
// No market named (a console whose browser language names none, the
// functest): the DEFAULT market's plan — only when every market sells
// exactly one plan, so "the plan of that market" means one thing.
let one_per_market = {
let mut seen = std::collections::BTreeSet::new();
plans
.values()
.all(|p| seen.insert(format!("{:?}", p.currency.market())))
};
let market = market.or_else(|| one_per_market.then_some(default_market));
if let Some(m) = market {
let mine: Vec<(&String, &crate::config::SignupPlan)> = plans
.iter()
.filter(|(_, p)| p.currency.market() == m)
.collect();
if let [(k, p)] = mine.as_slice() {
return Ok((k.as_str(), **p));
}
}
}
plan_for(plans, asked)
}
/// **One plan a new account may be bought on, as the sign-up page shows it**
/// (`Signup.Plans`, 2026-10-07): the page writes no price and no size of its own.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct PlanOnOffer {
pub id: String,
pub name: String,
pub resources_words: String,
pub price_words: String,
pub cap_bytes: u64,
pub period_days: u32,
}
/// The room as words, in the GB the front page's offer names (`10 GB`).
fn room_words(bytes: u64) -> String {
let gib = bytes as f64 / (1u64 << 30) as f64;
if (gib - gib.round()).abs() < 0.05 {
format!("{} GB", gib.round() as u64)
} else {
format!("{gib:.1} GB")
}
}
/// The period a plan's price covers, as words.
fn period_words(days: u32) -> String {
match days {
28..=31 => "a month".into(),
365 | 366 => "a year".into(),
1 => "a day".into(),
n => format!("for {n} days"),
}
}
/// The product's plans that sell ROOM — a plan with no price (an account at
/// the shop) is never on offer as room.
fn room_plans<'a>(
plans: &'a std::collections::BTreeMap<String, crate::config::SignupPlan>,
product: &str,
) -> Vec<(&'a String, &'a crate::config::SignupPlan)> {
let product = product.trim();
plans
.iter()
.filter(|(id, _)| product.is_empty() || id.split('/').next() == Some(product))
.filter(|(_, p)| p.amount_minor > 0)
.collect()
}
/// **The ONE market a new account's plans are offered in** (the sign-up page,
/// owner 2026-10-08: its − / + steps through SIZES, never through
/// currencies): the market asked when it has plans on offer, else the
/// deployment's default market when it has, else the first market that has
/// any (SE, FI, DK). `None`: nothing is on offer.
pub(crate) fn market_on_offer(
plans: &std::collections::BTreeMap<String, crate::config::SignupPlan>,
product: &str,
market: Option<monetize_pay::Market>,
default_market: monetize_pay::Market,
) -> Option<monetize_pay::Market> {
use monetize_pay::Market;
let on_offer = room_plans(plans, product);
market
.into_iter()
.chain([default_market, Market::Se, Market::Fi, Market::Dk])
.find(|m| on_offer.iter().any(|(_, p)| p.currency.market() == *m))
}
/// **The plans on offer to a new account**: the product's own (`gunnar/…`;
/// every plan when none is named), in ONE market — [`market_on_offer`]'s:
/// the customer's where it has any, else the deployment's default — smallest
/// room first, then cheapest. Never a mix of currencies (test2, 2026-10-08:
/// every market's plans to a browser naming none, and the page's + stepped
/// SEK → EUR → DKK).
pub(crate) fn plans_on_offer(
plans: &std::collections::BTreeMap<String, crate::config::SignupPlan>,
product: &str,
market: Option<monetize_pay::Market>,
default_market: monetize_pay::Market,
) -> Vec<PlanOnOffer> {
let Some(one) = market_on_offer(plans, product, market, default_market) else {
return Vec::new();
};
// An account at the shop sells no room: never on offer as room.
let mut shown: Vec<(&String, &crate::config::SignupPlan)> = room_plans(plans, product)
.into_iter()
.filter(|(_, p)| p.currency.market() == one)
.collect();
shown.sort_by_key(|(id, p)| (p.cap_bytes, p.amount_minor, (*id).clone()));
shown
.into_iter()
.map(|(id, p)| PlanOnOffer {
id: id.clone(),
name: room_words(p.cap_bytes),
resources_words: format!(
"{} of room, kept on the server and its replica",
room_words(p.cap_bytes)
),
price_words: format!(
"{} {}",
monetize_pay::Money {
minor: p.amount_minor,
currency: p.currency
}
.in_words(),
period_words(p.period_days)
),
cap_bytes: p.cap_bytes,
period_days: p.period_days,
})
.collect()
}
/// **`Signup.Start` for an account at the shop** ([`crate::signup::ACCOUNT_VENDOR`]):
/// a plan with no price and no room. The sink makes the account at once, the
/// record is CREATED under a session of the shop's own, and the answer leads to
/// the console's own welcome page (`success_url`), where the passkey is
/// registered as after any signup. A sink that refuses makes nothing that is
/// kept; nothing is owed, because nothing was paid.
fn start_account(
s: &AppState,
r: &pb::StartSignupRequest,
plan_id: &str,
plan: &crate::config::SignupPlan,
product: &str,
email: String,
(estate, site): (Option<&str>, Option<&str>),
) -> Result<pb::StartSignupResponse, Status> {
if plan.cap_bytes != 0 {
return Err(Status::failed_precondition(format!(
"plan {plan_id:?} has room and no price: room is never given away here — an account at the shop holds none ([signups.<product>.plans] amount_minor = 0 needs cap_bytes = 0)"
)));
}
let session = crate::signup::account_session().map_err(|why| {
Status::unavailable(format!("no session could be made for the account: {why}"))
})?;
let now = s.now() as i64;
let signup = monetize_pay::signup::PaidSignup {
vendor: crate::signup::ACCOUNT_VENDOR.to_owned(),
session: session.clone(),
reference: String::new(),
email,
username: r.username.clone(),
product: product.to_owned(),
plan: plan_id.to_owned(),
amount: monetize_pay::Money {
minor: 0,
currency: plan.currency,
},
tax: monetize_pay::Money {
minor: 0,
currency: plan.currency,
},
paid_at_unix_ms: now,
customer_country: None,
};
match make_account(s, &signup, None, estate, &r.success_url, site)? {
Made::Created | Made::Pending => Ok(pb::StartSignupResponse {
url: r.success_url.replace("{session}", &session),
session,
}),
Made::NameTaken(why) => Err(Status::failed_precondition(why)),
Made::Refused => Err(Status::failed_precondition(
crate::signup::words::COULD_NOT_CREATE,
)),
}
}
/// What [`make_account`] did.
enum Made {
/// The account exists (CREATED).
Created,
/// Kept PENDING: a pulled product's main makes it from the order it pulls.
Pending,
/// The name was taken (the customer's sentence).
NameTaken(String),
/// The sink refused; nothing is kept.
Refused,
}
/// **The account at the shop, made** (one writer for [`start_account`] and the card check's
/// finish): the sink makes it on the estate the caller is, the record is kept under
/// [`crate::signup::ACCOUNT_VENDOR`] with the checked card on it (`card`), and the sign-in
/// link (`success_url` with the session) is mailed. A sink that is unavailable is
/// UNAVAILABLE, and nothing is kept.
fn make_account(
s: &AppState,
signup: &monetize_pay::signup::PaidSignup,
card: Option<&crate::signup::CardOnFile>,
estate: Option<&str>,
success_url: &str,
site: Option<&str>,
) -> Result<Made, Status> {
let now = s.now() as i64;
let session = signup.session.as_str();
let to = signup.email.clone();
let kept = |state: Option<SignupState>| -> Result<(), Status> {
s.signups
.record_pending_on(signup, now, estate)
.map_err(store_err)?;
if let Some(card) = card {
s.signups
.set_card(crate::signup::ACCOUNT_VENDOR, session, card)
.map_err(store_err)?;
}
if let Some(state) = state {
s.signups
.set(crate::signup::ACCOUNT_VENDOR, session, state)
.map_err(store_err)?;
}
Ok(())
};
// ★ Made on the estate the caller is (its own bearer), and kept with the record so a
// later settle asks the same estate (ONE box, several sites of one product).
match s.signup_sink.paid_on(signup, estate) {
Ok(monetize_pay::signup::SignupOutcome::Created {
namespace,
username,
}) => {
kept(Some(SignupState::Created {
namespace,
username,
at_unix_ms: now,
}))?;
eprintln!(
"monetize-server: an account at the shop was made for {:?} ({}{})",
signup.username,
crate::signup::ACCOUNT_VENDOR,
if card.is_some() { ", card checked" } else { "" }
);
mail_sign_in_link(
s,
&signup.product,
&signup.username,
&to,
&success_url.replace("{session}", session),
site,
);
Ok(Made::Created)
}
Err(monetize_pay::signup::SignupError::NameTaken(why)) => Ok(Made::NameTaken(why)),
// **A pulled product** (5.4, G1): the estate's main makes the account from the order
// it pulls, so the record is kept PENDING under this session — `settle_pending` (and
// the welcome page's `Status` poll) finishes it once the main has acked. Answering
// UNAVAILABLE here kept nothing: the next Start minted a new session, a second order,
// and found the name taken by the first (money-flow 2026-10-10).
Err(monetize_pay::signup::SignupError::Unavailable(why))
if why.starts_with(crate::signup_pull::QUEUED) =>
{
kept(None)?;
eprintln!(
"monetize-server: an account at the shop for {:?} is pending ({}): {why}",
signup.username,
crate::signup::ACCOUNT_VENDOR
);
mail_sign_in_link(
s,
&signup.product,
&signup.username,
&to,
&success_url.replace("{session}", session),
site,
);
Ok(Made::Pending)
}
Err(
monetize_pay::signup::SignupError::Unavailable(why)
| monetize_pay::signup::SignupError::Maintenance(why),
) => Err(unavailable_for_customer(
"account",
&why,
crate::signup::words::SHOP_DOWN,
)),
Err(other) => {
eprintln!(
"monetize-server: an account at the shop for {:?} was refused by the sink: {other}",
signup.username
);
Ok(Made::Refused)
}
}
}
/// ★ **Pay once** (owner 2026-10-22, `DESIGN-PRIVATE-PAY-ONCE.md`): what a refused visitor
/// reads where this shop cannot make an account with a private order (no account plan).
pub const NO_ACCOUNT_WITH_ORDER: &str =
"An account cannot be made with this order here. Sign in to your account, or create it first.";
/// ★ **An estate order's own cancel page** (A10 row 6, `EstateOrder.Start{cancel_url}`): empty
/// stays empty (the vendor's own); else an https page on the order's site — the calling
/// estate's, else the sold product's home ([`site_refusal`]) — or the refusal.
pub(crate) fn order_cancel_page(
s: &AppState,
estate: Option<&str>,
cancel_url: &str,
) -> Result<String, Status> {
let cancel_url = cancel_url.trim();
if cancel_url.is_empty() {
return Ok(String::new());
}
if !cancel_url.starts_with("https://") || cancel_url.contains(char::is_whitespace) {
return Err(Status::invalid_argument(
"the cancel page is an https:// address",
));
}
let site_estate = match estate {
Some(e) => Some(e.to_owned()),
None => s.sold_product().ok().and_then(|p| s.registry.home_of(p)),
};
// As a signup's pages: checked against the site where one is known ([`page_refusal`]).
match site_estate
.as_deref()
.and_then(|e| s.registry.site_of(e))
.and_then(|site| site_refusal(&site, "", cancel_url))
{
Some(why) => Err(Status::invalid_argument(why)),
None => Ok(cancel_url.to_owned()),
}
}
pub(crate) fn new_account_for_order(
s: &AppState,
estate: Option<&str>,
username: &str,
email: &str,
welcome_url: &str,
) -> Result<crate::estate_order::account::NewAccount, Status> {
let product = match estate.and_then(|e| s.registry.product_of_estate(e)) {
Some(p) => p,
None => s
.sold_product()
.map_err(Status::failed_precondition)?
.to_owned(),
};
if !s.signup_sink.creates_accounts_for(&product) {
return Err(no_sink(&product));
}
let Some((plan, terms)) = s
.signup_plans
.snapshot_of(&product)
.into_iter()
.find(|(_, p)| crate::signup::is_account_plan(p) && p.cap_bytes == 0)
else {
eprintln!("monetize-server: a private order with a new account was refused: {product} sells no account plan ([signups.{product}.plans] amount_minor = 0, cap_bytes = 0)");
return Err(Status::failed_precondition(NO_ACCOUNT_WITH_ORDER));
};
let site_estate: Option<String> = match estate {
Some(e) => Some(e.to_owned()),
None if s.registry.sold_per_site(&product) => {
return Err(Status::permission_denied(format!("REFUSED [estate-unnamed] {product} is sold for several sites here and this order names none: its account and its sign-in mail are only ever on its estate's site")))
}
None => s.registry.home_of(&product),
};
let site = site_estate.as_deref().and_then(|e| s.registry.site_of(e));
let welcome_url = welcome_url.trim();
if !welcome_url.starts_with("https://") || !welcome_url.contains("{session}") {
return Err(Status::invalid_argument(
"the welcome page is an https:// address with {session} in it",
));
}
if let Some(why) = site
.as_deref()
.and_then(|site| page_refusal(site, welcome_url, ""))
{
return Err(Status::invalid_argument(why));
}
if let Some(why) = name_refusal(s, &product, username, estate)? {
return Err(Status::failed_precondition(why));
}
let email = email.trim();
if let Some(why) = monetize_pay::signup::email_refusal(email) {
return Err(Status::invalid_argument(why));
}
match s.signups.has_account_with_email_on(
&product,
estate,
s.registry.home_of(&product).as_deref(),
email,
) {
Ok(false) => {}
Ok(true) => {
return Err(Status::failed_precondition(
crate::signup::words::EMAIL_HAS_ACCOUNT,
))
}
Err(e) => return Err(store_err(e)),
}
if let Some(h) = s
.signups
.name_hold(
&product,
&hold_site(s, &product, estate),
username,
s.now() as i64,
)
.map_err(store_err)?
{
if !h.email.trim().eq_ignore_ascii_case(email) {
return Err(Status::failed_precondition(
crate::signup::words::NAME_BEING_BOUGHT,
));
}
}
let session = crate::signup::account_session().map_err(|why| {
Status::unavailable(format!("no session could be made for the account: {why}"))
})?;
Ok(crate::estate_order::account::NewAccount {
product,
plan,
currency: terms.currency,
username: username.to_owned(),
email: email.to_owned(),
welcome_url: welcome_url.to_owned(),
session,
estate: estate.map(str::to_owned),
site,
back_to: String::new(),
cancel_to: String::new(),
test_payer: String::new(),
})
}
/// ★ What a signed-in account reads when this shop holds no address for it and none was sent.
pub const NO_EMAIL_HERE: &str = "This account has no e-mail address here for the order's mail. Contact us, and nothing has been charged.";
/// ★ **The address a PROVEN account's order mails** (owner 2026-10-22: "man skall inte kunna
/// byta email"): the one the account was made with — the newest signup that created it on its
/// site — whatever the page sent. Only an account this shop holds no address for (one an
/// operator made by hand) takes the address an older page still sends; none sent: refused.
pub(crate) fn account_email(
s: &AppState,
estate: Option<&str>,
account: &str,
sent: &str,
) -> Result<String, Status> {
let product = match estate.and_then(|e| s.registry.product_of_estate(e)) {
Some(p) => p,
None => s
.sold_product()
.map_err(Status::failed_precondition)?
.to_owned(),
};
let home = s.registry.home_of(&product);
let away = estate.filter(|e| home.as_deref() != Some(*e));
let namespace = crate::site_ticket::tenant_on(s, &product, away, account)
.unwrap_or_else(|_| account.to_owned());
match s
.signups
.email_of(&product, &namespace)
.map_err(store_err)?
{
Some(own) => Ok(own),
None if !sent.trim().is_empty() => Ok(sent.trim().to_owned()),
None => Err(Status::failed_precondition(NO_EMAIL_HERE)),
}
}
/// ★ **Pay once: the name held for the order's payment page** — at least
/// [`crate::signup::NAME_HOLD_MS`], and as long as the page may be paid (`page_ms`).
pub(crate) fn hold_for_order(
s: &AppState,
na: &crate::estate_order::account::NewAccount,
page_ms: u64,
) -> Result<(), Status> {
let until = s.now() as i64 + crate::signup::NAME_HOLD_MS.max(page_ms as i64 + 5 * 60 * 1000);
let hold = crate::signup::NameHold {
vendor: crate::signup::ACCOUNT_VENDOR.to_owned(),
session: na.session.clone(),
email: na.email.clone(),
until_unix_ms: until,
};
s.signups
.hold_name(
&na.product,
&hold_site(s, &na.product, na.estate.as_deref()),
&na.username,
&hold,
)
.map_err(store_err)
}
/// ★ **Pay once: a held name let go** (the order was not started after all).
pub(crate) fn release_order_hold(s: &AppState, na: &crate::estate_order::account::NewAccount) {
if let Err(e) = s
.signups
.release_names_of(crate::signup::ACCOUNT_VENDOR, &na.session)
{
eprintln!(
"monetize-server: the name held for {:?} was not let go ({e}); it lapses by itself",
na.username
);
}
}
/// ★ **Pay once: the account a PAID private order makes** (owner 2026-10-22), as a paid
/// signup's: the sink makes it on the order's site (a pulled product's main when it next calls
/// monetize: PENDING, settled by [`crate::signup::settle_pending`]), its record is kept under
/// [`crate::signup::ACCOUNT_VENDOR`] with the order's session, and its sign-in link is mailed.
/// ONCE: a session that already has its record makes nothing. `Err`: why the paid order waits
/// (an ALERT when the account cannot be made at all — never laid for another's account).
pub(crate) fn make_paid_order_account(
s: &AppState,
reference: &str,
na: &crate::estate_order::account::NewAccount,
) -> Result<(), String> {
if s.signups
.get(crate::signup::ACCOUNT_VENDOR, &na.session)
.map_err(|e| format!("paid; its account cannot be looked up yet: signup store: {e}"))?
.is_some()
{
return Ok(());
}
let zero = monetize_pay::Money {
minor: 0,
currency: na.currency,
};
let signup = monetize_pay::signup::PaidSignup {
vendor: crate::signup::ACCOUNT_VENDOR.to_owned(),
session: na.session.clone(),
reference: reference.to_owned(),
email: na.email.clone(),
username: na.username.clone(),
product: na.product.clone(),
plan: na.plan.clone(),
amount: zero,
tax: zero,
paid_at_unix_ms: s.now() as i64,
customer_country: None,
};
match make_account(s, &signup, None, na.estate.as_deref(), &na.welcome_url, na.site.as_deref()) {
Ok(Made::Created | Made::Pending) => {
release_order_hold(s, na);
Ok(())
}
Ok(Made::NameTaken(why)) => Err(format!("ALERT estate-orders.account-not-made: paid, and its account {:?} could not be made ({why}); an operator sets it right — nothing is laid for another's account", na.username)),
Ok(Made::Refused) => Err(format!("ALERT estate-orders.account-not-made: paid, and the sink refused its account {:?}; an operator sets it right — nothing is laid", na.username)),
Err(status) => Err(format!("paid; its account {:?} is not made yet: {}", na.username, status.message())),
}
}
/// ★ **The vendor that checks a card for a free account on this site** (owner 2026-10-15),
/// or `Ok(None)` where the product is routed to NO vendor at all (an open build that takes
/// no money anywhere — the account is made as before). A site that sells through a vendor
/// makes a free account ONLY after that vendor checked a card.
fn card_check_vendor<'s>(
s: &'s AppState,
product: &str,
estate: Option<&str>,
) -> Result<Option<monetize_pay::Vendor<'s>>, Status> {
match s.registry.route_for(product, estate) {
Err(why) if why == s.registry.route_said(product) => Ok(None),
Err(why) => Err(Status::permission_denied(why)),
Ok(route) => match s.registry.find_vendor(&route) {
Some(v) => Ok(Some(v)),
None => {
eprintln!("monetize-server: a free {product} account was asked for and its vendor {route:?} is not loaded: no card check, no account");
Err(Status::failed_precondition(
crate::signup::words::CARD_CHECK_NOT_HERE,
))
}
},
}
}
/// ★ **`Signup.Start` for a free account on a site that sells through a vendor** (owner
/// 2026-10-15): nothing is made yet. The vendor mints a CARD CHECK — a page where the card is
/// checked and saved and nothing is charged (Stripe: Checkout `mode=setup`) — returning to
/// the console's own welcome page; what the account will be is kept here
/// ([`crate::signup::CardCheckStarted`]). The account is made when the welcome page asks
/// `Signup.Status` and the vendor says the card is checked ([`finish_card_check`]).
fn start_card_check(
s: &AppState,
vendor: monetize_pay::Vendor<'_>,
r: &pb::StartSignupRequest,
(plan_id, plan): (&str, &crate::config::SignupPlan),
product: &str,
email: String,
(estate, site): (Option<&str>, Option<&str>),
) -> Result<pb::StartSignupResponse, Status> {
if plan.cap_bytes != 0 {
return Err(Status::failed_precondition(format!(
"plan {plan_id:?} has room and no price: room is never given away here — an account at the shop holds none ([signups.<product>.plans] amount_minor = 0 needs cap_bytes = 0)"
)));
}
let host = site.map(str::to_owned).unwrap_or_else(|| {
r.success_url
.split("://")
.nth(1)
.unwrap_or_default()
.split('/')
.next()
.unwrap_or_default()
.to_owned()
});
let order = monetize_pay::signup::SignupOrder {
username: r.username.clone(),
email: email.clone(),
product: product.to_owned(),
plan: plan_id.to_owned(),
amount: monetize_pay::Money {
minor: 0,
currency: plan.currency,
},
market: plan.currency.market(),
description: format!("Card check for your free account at {host} — nothing is charged"),
success_url: r.success_url.clone(),
cancel_url: r.cancel_url.clone(),
};
match vendor.card_check_page(&order) {
Ok(Some(page)) if !page.session.trim().is_empty() => {
let started = crate::signup::CardCheckStarted {
product: product.to_owned(),
plan: plan_id.to_owned(),
username: r.username.clone(),
email,
currency: plan.currency,
estate: estate.map(str::to_owned),
site: site.map(str::to_owned),
success_url: r.success_url.clone(),
started_at_unix_ms: s.now() as i64,
refused: None,
};
s.signups
.record_card_check(vendor.name, &page.session, &started)
.map_err(store_err)?;
hold_for_page(
s,
product,
estate,
&r.username,
&started.email,
vendor.name,
&page.session,
)?;
eprintln!("monetize-server: a card check for a free {product} account {:?} was minted at {} ({})", r.username, vendor.name, page.session);
Ok(pb::StartSignupResponse {
url: page.url,
session: page.session,
})
}
Ok(_) => {
eprintln!("monetize-server: vendor {:?} checks no card (or named no session): no free {product} account is made without one", vendor.name);
Err(Status::failed_precondition(
crate::signup::words::CARD_CHECK_NOT_HERE,
))
}
Err(monetize_pay::PayError::Unavailable(why)) => Err(unavailable_for_customer(
"card check",
&format!("vendor: {why}"),
crate::signup::words::SHOP_DOWN,
)),
Err(e) => {
eprintln!(
"monetize-server: vendor {:?} would not mint a card check: {e}",
vendor.name
);
Err(Status::failed_precondition(
crate::signup::words::CARD_CHECK_NOT_HERE,
))
}
}
}
/// ★ The site a name is held on: the calling estate's, else the product's home.
fn hold_site(s: &AppState, product: &str, estate: Option<&str>) -> String {
estate
.map(str::to_owned)
.or_else(|| s.registry.home_of(product))
.unwrap_or_default()
}
/// ★ **Hold `username` for the vendor page `(vendor, session)`** for [`crate::signup::NAME_HOLD_MS`].
fn hold_for_page(
s: &AppState,
product: &str,
estate: Option<&str>,
username: &str,
email: &str,
vendor: &str,
session: &str,
) -> Result<(), Status> {
let hold = crate::signup::NameHold {
vendor: vendor.to_owned(),
session: session.to_owned(),
email: email.trim().to_owned(),
until_unix_ms: s.now() as i64 + crate::signup::NAME_HOLD_MS,
};
s.signups
.hold_name(product, &hold_site(s, product, estate), username, &hold)
.map_err(store_err)
}
/// ★ **`Signup.Status` for a paid signup this box minted and has no row for yet** (owner
/// 2026-10-15, B: a plan row goes to the vendor once; the account is made once the payment is
/// confirmed, monetize asking the vendor itself). `Ok(None)`: not such a session, or a vendor
/// that cannot say — the webhook decides, as before. Paid → kept and settled as a verified
/// webhook is (one settle path); open → PENDING; closed unpaid → REFUSED, the name free again.
pub fn finish_paid_signup(
s: &AppState,
vendor: &str,
session: &str,
) -> Result<Option<pb::SignupStatusResponse>, Status> {
use pb::signup_status_response::State as S;
if s.signups.get(vendor, session).map_err(store_err)?.is_some() {
return Ok(None);
}
let Some(estate) = s.signups.minted(vendor, session).map_err(store_err)? else {
return Ok(None);
};
let Some(v) = s.registry.find_vendor(vendor) else {
return Ok(None);
};
let answer = |state: S, words: &str| pb::SignupStatusResponse {
state: state as i32,
reason: if state == S::Refused {
words.to_owned()
} else {
String::new()
},
refusal: if state == S::Refused {
pb::signup_status_response::Refusal::Final as i32
} else {
0
},
words: words.to_owned(),
..Default::default()
};
match v.signup_standing(session) {
Ok(Some(monetize_pay::signup::SignupAtVendor::Paid(paid))) => {
s.signups
.record_pending_on(&paid, s.now() as i64, estate.as_deref())
.map_err(store_err)?;
let decided = crate::signup::settle(s, &paid);
eprintln!("monetize-server: signup {vendor}:{session} read PAID at the vendor by its return page: {decided:?}");
if !decided.is_pending() {
s.signups
.release_names_of(vendor, session)
.map_err(store_err)?;
}
Ok(crate::signup::status_with_retry(s, vendor, session)
.map_err(store_err)?
.map(|rec| status_response(&s.signups, &rec, s.now() as i64)))
}
Ok(Some(monetize_pay::signup::SignupAtVendor::Open)) => Ok(Some(answer(
S::Pending,
crate::signup::words::PAYMENT_WAITING,
))),
Ok(Some(monetize_pay::signup::SignupAtVendor::Closed)) => {
s.signups
.release_names_of(vendor, session)
.map_err(store_err)?;
Ok(Some(answer(
S::Refused,
crate::signup::words::PAYMENT_NOT_FINISHED,
)))
}
Ok(None) => Ok(None),
Err(e) => {
eprintln!("monetize-server: signup {vendor}:{session}: the vendor could not say where it stands ({e}); the webhook decides");
Ok(None)
}
}
}
/// A card check refused: final for the session, and its name is free again.
fn release_and_refuse(
s: &AppState,
vendor: &str,
session: &str,
words: &str,
) -> Result<(), StoreError> {
s.signups.release_names_of(vendor, session)?;
s.signups.refuse_card_check(vendor, session, words)
}
/// One card check's finish at a time in this process: the "does this card have a free
/// account here" read and the account it makes are one step, so two sessions with the
/// same card cannot both pass the read.
static CARD_GATE: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// A `Signup.Status` answer that is not a stored record (a card check not finished yet,
/// or refused before any account existed).
fn card_answer(
state: pb::signup_status_response::State,
started: &crate::signup::CardCheckStarted,
words: &str,
) -> pb::SignupStatusResponse {
let refused = state == pb::signup_status_response::State::Refused;
pb::SignupStatusResponse {
state: state as i32,
reason: if refused {
words.to_owned()
} else {
String::new()
},
product: started.product.clone(),
plan: started.plan.clone(),
refusal: if refused {
pb::signup_status_response::Refusal::Final as i32
} else {
0
},
words: words.to_owned(),
..Default::default()
}
}
/// ★ **`Signup.Status` for a card check this box minted** (owner 2026-10-15) — `Ok(None)` for
/// any other session. The vendor is asked where the check stands (never the return URL):
/// open → PENDING; closed unchecked → REFUSED; checked → the card's fingerprint is held
/// against the free accounts on this site — one there already is REFUSED
/// ([`crate::signup::words::CARD_HAS_ACCOUNT`]), else the account is made as at once
/// before ([`make_account`]) with the card on it, and its record answers from then on.
pub fn finish_card_check(
s: &AppState,
vendor: &str,
session: &str,
) -> Result<Option<pb::SignupStatusResponse>, Status> {
use pb::signup_status_response::State as S;
let Some(started) = s.signups.card_check(vendor, session).map_err(store_err)? else {
return Ok(None);
};
let account = |s: &AppState| -> Result<Option<pb::SignupStatusResponse>, Status> {
Ok(
crate::signup::status_with_retry(s, crate::signup::ACCOUNT_VENDOR, session)
.map_err(store_err)?
.map(|rec| status_response(&s.signups, &rec, s.now() as i64)),
)
};
if let Some(answer) = account(s)? {
return Ok(Some(answer));
}
if let Some(words) = &started.refused {
return Ok(Some(card_answer(S::Refused, &started, words)));
}
let Some(v) = s.registry.find_vendor(vendor) else {
return Err(unavailable_for_customer(
"card check",
&format!("vendor {vendor:?} is not loaded"),
crate::signup::words::SHOP_DOWN,
));
};
let check = match v.card_check(session) {
Ok(Some(c)) => c,
Ok(None) => {
return Err(unavailable_for_customer(
"card check",
&format!("vendor {vendor:?} cannot say where card check {session} stands"),
crate::signup::words::SHOP_DOWN,
))
}
Err(monetize_pay::PayError::Unavailable(why)) => {
return Err(unavailable_for_customer(
"card check",
&format!("vendor: {why}"),
crate::signup::words::SHOP_DOWN,
))
}
Err(e) => {
eprintln!("monetize-server: card check {vendor}:{session}: the vendor said {e}");
return Err(unavailable_for_customer(
"card check",
&format!("vendor: {e}"),
crate::signup::words::SHOP_DOWN,
));
}
};
match check.state {
monetize_pay::signup::CardCheckState::Open => {
return Ok(Some(card_answer(
S::Pending,
&started,
crate::signup::words::CARD_BEING_CHECKED,
)))
}
monetize_pay::signup::CardCheckState::Expired => {
release_and_refuse(s, vendor, session, crate::signup::words::CARD_NOT_CHECKED)
.map_err(store_err)?;
return Ok(Some(card_answer(
S::Refused,
&started,
crate::signup::words::CARD_NOT_CHECKED,
)));
}
monetize_pay::signup::CardCheckState::Checked => {}
}
if check.fingerprint.trim().is_empty() {
return Err(unavailable_for_customer(
"card check",
&format!("vendor {vendor:?} answered card check {session} checked with no fingerprint"),
crate::signup::words::SHOP_DOWN,
));
}
let _one = CARD_GATE.lock().unwrap_or_else(|p| p.into_inner());
// Another poll may have made it while this one waited at the gate.
if let Some(answer) = account(s)? {
return Ok(Some(answer));
}
let home = s.registry.home_of(&started.product);
if let Some((held, _)) = s
.signups
.free_account_with_card_on(
&started.product,
started.estate.as_deref(),
home.as_deref(),
vendor,
&check.fingerprint,
)
.map_err(store_err)?
{
eprintln!("monetize-server: card check {vendor}:{session}: the card already has a free {} account here ({held}); no second one", started.product);
release_and_refuse(s, vendor, session, crate::signup::words::CARD_HAS_ACCOUNT)
.map_err(store_err)?;
return Ok(Some(card_answer(
S::Refused,
&started,
crate::signup::words::CARD_HAS_ACCOUNT,
)));
}
let now = s.now() as i64;
let signup = monetize_pay::signup::PaidSignup {
vendor: crate::signup::ACCOUNT_VENDOR.to_owned(),
session: session.to_owned(),
reference: check.reference.clone(),
email: started.email.clone(),
username: started.username.clone(),
product: started.product.clone(),
plan: started.plan.clone(),
amount: monetize_pay::Money {
minor: 0,
currency: started.currency,
},
tax: monetize_pay::Money {
minor: 0,
currency: started.currency,
},
paid_at_unix_ms: now,
customer_country: None,
};
let card = crate::signup::CardOnFile {
vendor: vendor.to_owned(),
fingerprint: check.fingerprint.trim().to_owned(),
customer: check.customer.clone(),
payment_method: check.payment_method.clone(),
reference: check.reference.clone(),
checked_at_unix_ms: now,
};
match make_account(
s,
&signup,
Some(&card),
started.estate.as_deref(),
&started.success_url,
started.site.as_deref(),
)? {
Made::Created | Made::Pending => {
s.signups
.release_names_of(vendor, session)
.map_err(store_err)?;
account(s)
}
Made::NameTaken(why) => {
let words = format!("{why} Nothing has been charged — start again with another name.");
release_and_refuse(s, vendor, session, &words).map_err(store_err)?;
Ok(Some(card_answer(S::Refused, &started, &words)))
}
Made::Refused => {
release_and_refuse(s, vendor, session, crate::signup::words::COULD_NOT_CREATE)
.map_err(store_err)?;
Ok(Some(card_answer(
S::Refused,
&started,
crate::signup::words::COULD_NOT_CREATE,
)))
}
}
}
/// **The account's sign-in link, by mail** (OM-20): the welcome page the answer leads to is
/// also mailed to the address the account was made for, so a buyer who closes the tab still
/// reaches it. The link is the secret and is only in the mail's body. A mail that is not
/// sent is logged and fails nothing: the account is made, and the page still leads on.
pub(crate) fn mail_sign_in_link(
s: &AppState,
product: &str,
username: &str,
to: &str,
url: &str,
site: Option<&str>,
) {
match sign_in_mail(s, product, username, to, url, site) {
Ok(sent) => eprintln!("monetize-server: the sign-in link for {username:?} was mailed: {sent}"),
Err(e) => eprintln!("monetize-server: the sign-in link for {username:?} was NOT mailed to {} ({e:?}); the account is made", monetize_mail::masked(to)),
}
}
/// The sign-in mail itself: the link in the body only, the host in the subject.
/// `site`: the estate's own site (ST-23) — the host the mail names; the link is on it
/// (checked at Start). `None` only where no site is declared at all.
fn sign_in_mail(
s: &AppState,
product: &str,
username: &str,
to: &str,
url: &str,
site: Option<&str>,
) -> Result<String, monetize_pay::signup::SignupError> {
let host = site.unwrap_or_else(|| {
url.split("://")
.nth(1)
.unwrap_or(url)
.split('/')
.next()
.unwrap_or_default()
});
let body = format!(
"Your account {username} at https://{host} is made. This link registers your passkey on it, while it has none:\n\n {url}\n\n\
After that, sign in at https://{host} with your passkey. If you did not sign up, you can ignore this mail.\n"
);
let notice = monetize_pay::CustomerNotice {
product: product.to_owned(),
tenant: username.to_owned(),
to: to.to_owned(),
kind: "account.sign_in".into(),
subject: format!("Your sign-in link for https://{host}"),
body,
};
s.signup_sink.notify(¬ice)
}
/// How often one signup's sign-in link may be sent again.
pub const SIGN_IN_AGAIN_EVERY_MS: u64 = 60_000;
/// **`Signup.SendSignInAgain`** (owner 2026-10-11: the welcome page says where the link went,
/// with [Send again]): a CREATED signup's link — `welcome_url` with its session — mailed
/// again to the address the signup was made with. The signup is found by its session, for
/// `product`, from the site that asks (another site's is not found); the page is on the
/// signup's site (`[signup-other-site]`) and https, with `{session}` in it; once a minute per
/// session. The answer names the address MASKED only.
pub fn send_sign_in_again(
s: &AppState,
product: &str,
session: &str,
welcome_url: &str,
estate: Option<&str>,
) -> Result<pb::SignupSignInAgainResponse, Status> {
static SENT: std::sync::OnceLock<std::sync::Mutex<std::collections::BTreeMap<String, u64>>> =
std::sync::OnceLock::new();
let session = session.trim();
let missing = || Status::not_found(format!("no {product} signup {session:?} here"));
let rec = s
.signups
.list(0)
.map_err(store_err)?
.into_iter()
.find(|r| !session.is_empty() && r.signup.session == session && r.signup.product == product)
.ok_or_else(missing)?;
if let Some(caller) = estate {
let home = s.registry.home_of(product);
if rec.estate.as_deref().or(home.as_deref()) != Some(caller) {
return Err(missing());
}
}
let SignupState::Created { username, .. } = &rec.state else {
return Err(Status::failed_precondition(
"This account is not made yet: its sign-in link is mailed once it is.",
));
};
let url = welcome_url.trim();
if !url.starts_with("https://") || !url.contains("{session}") {
return Err(Status::invalid_argument(
"the welcome page is an https:// address with {session} in it",
));
}
// ST-23: the link is on the signup's own site (its estate's, else the home's), always.
let site = estate
.map(str::to_owned)
.or_else(|| rec.estate.clone())
.or_else(|| s.registry.home_of(product))
.and_then(|e| s.registry.site_of(&e));
if site.is_none() && s.registry.sold_per_site(product) {
return Err(Status::permission_denied(format!("REFUSED [estate-unnamed] {product} is sold for several sites here and this signup's site is not known: its sign-in link is only ever on its estate's site")));
}
if let Some(why) = site.as_deref().and_then(|site| page_refusal(site, url, "")) {
return Err(Status::invalid_argument(why));
}
let now = s.now();
{
let mut sent = SENT
.get_or_init(Default::default)
.lock()
.unwrap_or_else(|p| p.into_inner());
if sent
.get(session)
.is_some_and(|at| now.saturating_sub(*at) < SIGN_IN_AGAIN_EVERY_MS)
{
return Err(Status::resource_exhausted("It was sent a moment ago. Look in your inbox (and spam), or try again in a minute."));
}
sent.insert(session.to_owned(), now);
}
let to = rec.signup.email.trim();
match sign_in_mail(
s,
product,
username,
to,
&url.replace("{session}", session),
site.as_deref(),
) {
Ok(sent) => {
eprintln!(
"monetize-server: the sign-in link for {username:?} was mailed again: {sent}"
);
Ok(pb::SignupSignInAgainResponse {
words: format!("We’ve sent it again to {}.", monetize_mail::masked(to)),
})
}
Err(e) => {
eprintln!("monetize-server: the sign-in link for {username:?} was NOT mailed again to {} ({e:?})", monetize_mail::masked(to));
Err(Status::unavailable(
"The mail could not be sent right now. Try again in a while.",
))
}
}
}
/// **Is this name free?** The shape rule, then the sink — in that order, because
/// the shape rule names the offending character and costs no call.
///
/// A name is free or taken FOR a product (one monetize sells gunnar's and
/// holger's accounts, each from its own sink); the product is the caller's.
///
/// One writer for `CheckName` and `Start`: the check a console makes is a
/// courtesy and the one `Start` makes is the gate, and two spellings of one
/// question is two answers that can disagree.
/// The product a Signup call is FOR, or INVALID_ARGUMENT: one monetize sells
/// several products, and none is assumed.
fn product_named(product: &str, verb: &str) -> Result<String, Status> {
let p = product.trim();
if p.is_empty() {
return Err(Status::invalid_argument(format!(
"Signup.{verb} names no product: one monetize sells several, and a call says which"
)));
}
Ok(p.to_owned())
}
/// UNIMPLEMENTED for a product with no sink that creates accounts.
fn no_sink(product: &str) -> Status {
Status::unimplemented(format!("no signup sink is loaded for {product}: this server creates no {product} account, so it sells none ([signups.{product}] guest = … is the sink)"))
}
fn name_refusal(
s: &AppState,
product: &str,
username: &str,
estate: Option<&str>,
) -> Result<Option<String>, Status> {
if let Some(why) = monetize_pay::signup::username_refusal(username) {
return Ok(Some(why));
}
// On the estate the account would be made on (the caller's), never another site's names.
match s.signup_sink.name_verdict_on(product, username, estate) {
Ok(monetize_pay::signup::NameVerdict::Free) => Ok(None),
Ok(monetize_pay::signup::NameVerdict::Taken(why)) => Ok(Some(why)),
// **Not "taken".** A console that said "choose another username"
// because the appliance was rebooting would be lying to somebody whose
// name is free, and they would spend it on a second-choice name.
// The product's own words are for the log; a visitor reads that the
// shop is down, not what gunnar said about a credential.
Err(monetize_pay::signup::SignupError::Unavailable(why)) => Err(unavailable_for_customer(
"name check",
&why,
crate::signup::words::SHOP_DOWN,
)),
Err(other) => Ok(Some(other.to_string())),
}
}
/// A `Signup.Start` past this is said at WARN (the front raises a line that
/// says FAILED): a healthy one is the vendor's one round trip.
pub const START_BUDGET: std::time::Duration = std::time::Duration::from_secs(5);
/// **Where one `Signup.Start` spent its time**, said as ONE line when it ends
/// — minted, refused or failed. Each stage is the time since the previous mark.
pub struct StartTimings {
user: String,
began: std::time::Instant,
last: std::time::Instant,
marks: Vec<(String, u128)>,
pub outcome: &'static str,
say: fn(&str),
}
impl StartTimings {
pub fn new(user: &str) -> StartTimings {
StartTimings::saying(user, |l| eprintln!("{l}"))
}
pub fn saying(user: &str, say: fn(&str)) -> StartTimings {
let now = std::time::Instant::now();
StartTimings {
user: user.to_owned(),
began: now,
last: now,
marks: Vec::new(),
outcome: "not minted",
say,
}
}
pub fn mark(&mut self, stage: &str) {
self.mark_owned(stage.to_owned());
}
pub fn mark_owned(&mut self, stage: String) {
let now = std::time::Instant::now();
self.marks
.push((stage, now.duration_since(self.last).as_millis()));
self.last = now;
}
/// The line, for a total of `total`.
pub fn line(&self, total: std::time::Duration) -> String {
let stages = self
.marks
.iter()
.map(|(s, ms)| format!("{s} {ms} ms"))
.collect::<Vec<_>>()
.join(" · ");
let over = if total > START_BUDGET {
format!(
" — SLOW: past the {} s a signup may take (budget FAILED)",
START_BUDGET.as_secs()
)
} else {
String::new()
};
format!(
"monetize-server: signup start {:?}: {} in {} ms — {stages}{over}",
self.user,
self.outcome,
total.as_millis()
)
}
}
impl Drop for StartTimings {
fn drop(&mut self) {
(self.say)(&self.line(self.began.elapsed()));
}
}
/// **How a `Signup.Start` is paid.**
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PayBy {
/// The customer's way: the vendor mints its hosted page and the webhook settles it.
Checkout,
/// **The operator's TEST BUTTON** (2026-10-11): the vendor, on a TEST key
/// only, takes the money with its own test payment method and answers the
/// paid signup its webhook would have carried; it is settled through the
/// ONE settle path every webhook takes ([`crate::signup::settle`]).
/// Everything before the vendor — the name, the address, the plan, the
/// room, the sold capacity — is the customer's path, unchanged. A FREE account asked for
/// this way (the button's private-instance run) is made at once, without the card check
/// a customer's free account takes (owner 2026-10-15): the operator's own test account.
TestPayment,
}
/// Pure: **a signup's pages must be on its site** — `https://<site>/…` for the success and
/// the cancel page alike (an empty one is the vendor's own default and passes). `None`: both
/// are; else the refusal, naming the page and the site.
pub fn site_refusal(site: &str, success_url: &str, cancel_url: &str) -> Option<String> {
let origin = format!("https://{site}/");
// WWW-CANON (owner 2026-10-30): the site's www name is the site, and its console's origin.
let www = format!("https://www.{site}/");
[("success", success_url), ("cancel", cancel_url)]
.into_iter()
.find(|(_, u)| !u.is_empty() && !u.starts_with(&origin) && !u.starts_with(&www))
.map(|(which, u)| format!("REFUSED [signup-other-site] the {which} page {u:?} is not on this estate's site {site}: a signup returns where it began"))
}
/// What the test button reads when the routed vendor is not on a test key.
pub const NOT_A_TEST_KEY: &str = "the vendor's key is not a TEST key (live, or it cannot say): a test button never spends live money";
/// **`Signup.Start`, the one implementation** — the sign-up page's call
/// ([`PayBy::Checkout`]) and the operator's test button ([`PayBy::TestPayment`]).
/// `estate`: the site the call comes from ([`crate::auth::CallerEstate`]); a product with
/// `[estates]` declared bills ONLY through that site's own account
/// ([`crate::registry::Registry::route_for`]), and its pages must be on that site
/// ([`site_refusal`]).
pub fn start_signup(
s: &AppState,
r: &pb::StartSignupRequest,
pay: PayBy,
estate: Option<&str>,
) -> Result<pb::StartSignupResponse, Status> {
start_signup_in(s, r, pay, estate)
}
/// A signup's pages are on its site ([`site_refusal`]). (A33-12: the ONE shop's own welcome page
/// is no exception any more: its sign-up page is deleted.)
pub fn page_refusal(site: &str, success_url: &str, cancel_url: &str) -> Option<String> {
site_refusal(site, success_url, cancel_url)
}
fn start_signup_in(
s: &AppState,
r: &pb::StartSignupRequest,
pay: PayBy,
estate: Option<&str>,
) -> Result<pb::StartSignupResponse, Status> {
// One line when this returns, whichever way: the total and where
// it went (test2 2026-09-30: 25 s with nothing logged in between).
let mut timed = StartTimings::new(&r.username);
// A checkout is minted FOR a product: its plans, its sink, its
// vendor instance. None named mints nothing.
let product = product_named(&r.product, "Start")?;
// **No account can be made here, so none is sold** (owner
// 2026-09-26): with no sink that creates one, a checkout would take
// a stranger's money for an account that never exists.
// UNIMPLEMENTED — the answer a console already turns into "this
// console cannot sell an account".
if !s.signup_sink.creates_accounts_for(&product) {
return Err(no_sink(&product));
}
// ★ ST-23: **a signup begins on ONE site, and its pages (and the account's mail) are on
// it** — the calling estate's, else the product's home where it is not sold per site. A
// product sold for several sites refuses a Start that names none: the sign-in mail would
// otherwise link to whatever host `success_url` names.
let site_estate: Option<String> = match estate {
Some(e) => Some(e.to_owned()),
None if s.registry.sold_per_site(&product) => {
return Err(Status::permission_denied(format!(
"REFUSED [estate-unnamed] {product} is sold for several sites here and this signup names none (the estate's own bearer names it): its pages and its sign-in mail are only ever on its estate's site"
)))
}
None => s.registry.home_of(&product),
};
let site = site_estate.as_deref().and_then(|e| s.registry.site_of(e));
if let Some(why) = site
.as_deref()
.and_then(|site| page_refusal(site, &r.success_url, &r.cancel_url))
{
return Err(Status::invalid_argument(why));
}
let named = name_refusal(s, &product, &r.username, estate);
timed.mark("name");
if let Some(reason) = named? {
return Err(Status::failed_precondition(reason));
}
// **The contact address, before any card form.** Required: a signup
// whose only address is whatever a wallet handed the vendor is one
// the buyer never chose (measured 2026-09-15). INVALID_ARGUMENT, so a
// console shows the sentence and the buyer fixes one field.
let email = r.email.trim().to_owned();
if let Some(reason) = monetize_pay::signup::email_refusal(&email) {
return Err(Status::invalid_argument(reason));
}
// **One address, one account** (PLAN §3c, OM-19 (4)): an address
// that already has an account here signs in to it — a second
// account is never made for it, and nothing is merged by email.
// ★ ST-6: per SITE — an address with an account on another site signs up here.
match s.signups.has_account_with_email_on(
&product,
estate,
s.registry.home_of(&product).as_deref(),
&email,
) {
Ok(false) => {}
Ok(true) => {
return Err(Status::failed_precondition(
crate::signup::words::EMAIL_HAS_ACCOUNT,
))
}
Err(e) => return Err(Status::internal(format!("signup store: {e}"))),
}
// ★ **A name held for somebody else's open vendor page** (owner 2026-10-15, B): nobody
// takes it while that payment (or card check) runs. The same buyer starting again is not
// shut out by their own page.
// ★ A33-2: the test button's press takes this check too: only its card differs.
if let Some(h) = s
.signups
.name_hold(
&product,
&hold_site(s, &product, estate),
&r.username,
s.now() as i64,
)
.map_err(store_err)?
{
if !h.email.trim().eq_ignore_ascii_case(&email) {
return Err(Status::failed_precondition(
crate::signup::words::NAME_BEING_BOUGHT,
));
}
}
let market = match monetize_proto::Market::try_from(r.market) {
Ok(monetize_proto::Market::Se) => Some(monetize_pay::Market::Se),
Ok(monetize_proto::Market::Fi) => Some(monetize_pay::Market::Fi),
Ok(monetize_proto::Market::Dk) => Some(monetize_pay::Market::Dk),
_ => None,
};
let plans_now = s.signup_plans.snapshot_of(&product);
let default_market = s
.terms
.read()
.unwrap_or_else(|e| e.into_inner())
.default_market;
// ★ Room by the rule (round 5.3.1): a PRICED id is sold only at the
// price it names, and only while that is today's — the checkout
// charges exactly what the page showed, or nothing is started.
let priced: Option<(String, crate::config::SignupPlan)> = match crate::plans::parse_priced_id(
&r.plan,
) {
None => None,
Some((of, _, _)) if of != product => {
return Err(Status::invalid_argument(format!(
"plan {:?} is {of}'s, and this checkout is for {product}",
r.plan
)))
}
Some((_, _, price)) => {
let in_market = market.unwrap_or_else(|| price.currency.market());
match crate::plans::priced_id_is_current(s, &r.plan, in_market) {
Ok(true) => Some((r.plan.clone(), crate::plans::priced_signup_plan(s, &r.plan).ok_or_else(|| Status::invalid_argument(format!("plan {:?} is not sold here", r.plan)))?)),
Ok(false) => return Err(Status::failed_precondition("The price of this plan changed while the page was open. Look again: nothing has been charged.")),
Err(why) => return Err(Status::failed_precondition(why)),
}
}
};
let (plan_id, plan) = match &priced {
Some((id, p)) => (id.as_str(), *p),
None => plan_for_market(&plans_now, &r.plan, market, default_market)?,
};
// The plan is the product's own (`gunnar/10gb-1m` is one bundle of
// one product), and the sink reads `metadata[product]` back off the
// session to know whose account it is making.
if crate::signup::plan_product(plan_id) != product {
return Err(Status::invalid_argument(format!(
"plan {plan_id:?} is not {product}'s"
)));
}
// ★ **An account at the shop, without room here** (one shop, owner
// 2026-10-07): a plan with no price sells nothing, so it asks no box
// for room and no vendor for money — the account is made now.
if plan.amount_minor == 0 {
timed.outcome = "account";
// ★ **A free account only after a card check** (owner 2026-10-15), on every site
// that sells through a vendor; a product routed to no vendor at all (an open
// build) makes it at once, as before.
// ★ A33-2/A33-3: no account is made around the card check: a free account the test
// button asked for where a card check stands is refused by name (its private press is a
// visitor's order, whose account is made when the order is paid).
let checks = card_check_vendor(s, &product, estate)?;
if checks.is_some() && pay == PayBy::TestPayment {
timed.outcome = "test refused";
return Err(Status::failed_precondition("a free account is made only after a card check: the test button buys a paid plan or a private instance, never a free account by itself"));
}
if let Some(vendor) = checks {
timed.outcome = "card check";
return start_card_check(
s,
vendor,
r,
(plan_id, &plan),
&product,
email,
(estate, site.as_deref()),
);
}
return start_account(
s,
r,
plan_id,
&plan,
&product,
email,
(estate, site.as_deref()),
);
}
// **THE BOX IS ASKED WHETHER IT HAS THE ROOM, BEFORE THE CARD.**
//
// A purchase has asked since the order model was written
// (`Transaction::quote` → `Product::can_absorb`, whose own test is
// "No money moves for capacity that cannot be delivered"). A signup
// asked nobody: its three checks are the name, the plan and the
// amount, and `signup_sales` recorded each one with "the room was on
// hand" — an assumption never once put to the appliance. On a full
// box the shop went on minting live checkouts, every one a real
// charge for a cap the box cannot honour and a refund by hand to
// undo.
//
// HERE and not in the webhook, because this is the last moment a
// capacity refusal is free: after the webhook the money has moved.
//
// A deployment with no product linked — the open build — is not
// gated by a box it does not have. A product that will not ANSWER is
// refused like a full one: neither is a box that can be sold.
if let Some(p) = s.registry.products().find(|p| p.id() == product) {
let caps = std::collections::BTreeMap::from([(
s.signup_plans.cap_meter_of(&product),
plan.cap_bytes,
)]);
// **NO TENANT ID, and that is the whole question.** A signup is
// asking "could you serve N bytes that are not here yet"; a
// purchase is asking "could you raise THIS tenant to N", and
// gunnar's `Admin.Capacity` says in its own proto which field
// picks between them: *"Empty asks the harder question … which
// is the right one for a brand-new tenant and the wrong one for
// a raise."*
//
// Naming the username here asked the RAISE question about an
// account that by definition does not exist yet. gunnar answers
// `NOT_FOUND` — deliberately, because quoting a raise against
// the wrong tenant's zero is how a seller oversells — the
// plugin turns that into "no tenant to raise", and
// `Capacity::Unanswered` is refused like a full box, because
// silence is not a yes.
//
// MEASURED on the front 2026-09-18: every signup was refused
// with NO_ROOM on a box with room. The log said so exactly —
// *"gunnar's catalog does not hold the namespace \"henrilina\",
// so there is no tenant to raise … THAT IS A MEASUREMENT THAT
// DID NOT HAPPEN, not a full disk"* — and the refusal chain was
// right at every step but this one. The empty id is what
// `GunnarProduct::servable` already passes for the same reason.
// …with the queue past the room (round 5.3): a full box whose main
// grows its own set when told still sells, and the main is told.
// ★ ST-11: a signup from ANOTHER site of the product asks that site's own main. A
// pulled product routes `@<estate>/<name>` to the estate's report, where a name not
// promised anything yet is the same brand-new-tenant question; the home asks as above.
let asked = match estate.filter(|e| s.registry.home_of(&product).as_deref() != Some(*e)) {
Some(e) => monetize_product::TenantId::on_estate(e, &r.username),
None => monetize_product::TenantId(String::new()),
};
let absorbed = crate::grow::can_absorb_queued(s, p, &asked, &caps);
timed.mark("capacity");
if let Err(why) = absorbed {
return Err(unavailable_for_customer(
"capacity",
&format!("{product}: {why}"),
crate::signup::words::NO_ROOM,
));
}
}
// **AND WHAT HAS ALREADY BEEN SOLD**, which the question above does
// not ask. `can_absorb` is `Admin.Capacity` against FREE space, and
// free space is what nobody has written yet: eleven tenants holding
// a tenth of their 10 GiB caps leave a box that answers yes to
// every one of them (measured on the front 2026-09-18). The caps
// are the promise, and the caps are what breaks.
//
// `monetize::fleet::Oversell` has computed `sum(sold) − servable`
// since it was written and gated nothing — it is rendered on the
// Usage page. This is that same fold, with THIS sale added, against
// the allowance the operator wrote down. See `crate::oversell` for
// the knob, and for what the hold below does and does not close.
//
// The hold lives to the end of this closure: a slot is held from
// here until the checkout is minted, so two `Signup.Start` calls
// racing for the last one cannot both be told yes. Dropping it —
// any error path below, including the vendor refusing to mint —
// frees the slot.
let hold = match s.registry.products().find(|p| p.id() == product) {
Some(p) => {
let admitted = crate::oversell::admit_on(s, p, plan.cap_bytes, estate);
timed.mark("sold-capacity");
match admitted {
Ok(hold) => Some(hold),
// ★ Round 5.3 (owner 2026-10-08: "the customer must not
// know about a cap! it must be queued and resized
// later"): where the set is resized later, past the
// sold-capacity wall is SOLD and QUEUED at the estate that
// grows its own set when told; the operator is told.
Err(why) if crate::grow::queues_for_resize(s, p) => {
// Once per signup, as INFO (round 5.3): queued is not a refusal.
if let Some(line) = crate::oversell::queue_note(
&format!("signup:{product}/{}", r.username),
&format!("{product}/{}", r.username),
&why,
) {
eprintln!("{line}");
}
None
}
Err(why) => {
return Err(unavailable_for_customer(
"capacity",
&why,
crate::signup::words::NO_ROOM,
))
}
}
}
// No product linked: the open build is not gated by a box it
// does not have, exactly as `can_absorb` above is not.
None => None,
};
let market = plan.currency.market();
// UNIMPLEMENTED, like no sink: "not sold here" is a configuration
// gap a console tells apart by CODE (stinky gunnar-ui #7), never by
// these words — FAILED_PRECONDITION is a refusal about the order.
// An estate-routed product: the site's own account, its pages on its site — or a
// refusal naming what is missing, never another site's account.
if plan.price().minor != 0 {
match s.registry.route_for(&product, estate) {
// Routed NOWHERE (no `vendor`, no site): the configuration gap the
// UNIMPLEMENTED below names, not a refusal about this order.
Err(why) if why == s.registry.route_said(&product) => {}
Err(why) => return Err(Status::permission_denied(why)),
Ok(_) => {}
}
}
let vendor = s
.registry
.vendor_for_estate(&plan.price(), market, &product, estate, &r.vendor)
.ok_or_else(|| {
Status::unimplemented(format!(
"no vendor here can sell a {product} signup at {} in {:?}: {}; serving: {:?}",
plan.price().in_words(),
market,
s.registry.route_said(&product),
s.registry.vendor_names()
))
})?;
// What the vendor's page names: the product, its room and period, and the price (A16).
let description = crate::signup::signup_checkout_words(&product, &plan);
let order = monetize_pay::signup::SignupOrder {
username: r.username.clone(),
email,
product,
plan: plan_id.to_owned(),
// **NET.** `monetize_server::signup::refusal` compares the plan
// price against `amount - tax`, so what the vendor is asked for
// is the price before whatever tax it calculates on top.
amount: plan.price(),
market,
// In MAJOR units: this is what the buyer reads on the vendor's
// page, and `10000 SEK` for a 100,00 kr plan is a hundredfold lie.
description,
success_url: r.success_url.clone(),
cancel_url: r.cancel_url.clone(),
};
if pay == PayBy::TestPayment {
return pay_as_test(s, vendor, &order, hold, &r.success_url, estate, &mut timed);
}
let minted = vendor.signup_page(&order);
timed.mark_owned(format!("vendor {}", vendor.name));
match minted {
Ok(Some(checkout)) => {
// ★ ST-7: this box settles only the sessions it minted — kept here, with the
// site, before the buyer is sent to pay. A vendor that names no session at mint
// cannot be told apart from another system's money on the same account.
if checkout.session.trim().is_empty() {
return Err(Status::unimplemented(format!(
"vendor {:?} minted a checkout with no session id: this monetize settles only the sessions it minted (ST-7), so it does not sell signups through it",
vendor.name
)));
}
s.signups.record_minted(vendor.name, &checkout.session, estate).map_err(store_err)?;
s.signups.record_welcome(vendor.name, &checkout.session, &order.success_url).map_err(store_err)?;
// ★ The name is held while the payment page runs (owner 2026-10-15, B).
hold_for_page(s, &order.product, estate, &order.username, &order.email, vendor.name, &checkout.session)?;
timed.outcome = "minted";
// The checkout exists: the slot stays held until the
// webhook settles the session or the hold's TTL runs out.
if let Some(hold) = hold {
hold.minted(vendor.name, &checkout.session);
}
Ok(pb::StartSignupResponse { url: checkout.url, session: checkout.session })
}
Ok(None) => Err(Status::unimplemented(format!(
"vendor {:?} mints no signup checkout; this deployment's signups are sold some other way",
vendor.name
))),
Err(monetize_pay::PayError::Unavailable(why)) => Err(unavailable_for_customer("checkout", &format!("vendor: {why}"), crate::signup::words::SHOP_DOWN)),
Err(e) => Err(Status::failed_precondition(format!("vendor: {e}"))),
}
}
/// [`PayBy::TestPayment`]'s vendor step: refused unless the vendor's key is a
/// TEST key; the vendor pays with its test payment method and answers the paid
/// signup; it is kept and settled exactly as a verified webhook is.
fn pay_as_test(
s: &AppState,
vendor: monetize_pay::Vendor<'_>,
order: &monetize_pay::signup::SignupOrder,
hold: Option<crate::oversell::Hold<'_>>,
success_url: &str,
estate: Option<&str>,
timed: &mut StartTimings,
) -> Result<pb::StartSignupResponse, Status> {
if vendor.livemode() != Some(false) {
timed.outcome = "test refused";
return Err(Status::failed_precondition(format!(
"{}: {NOT_A_TEST_KEY}",
vendor.name
)));
}
let paid = vendor.test_signup(order);
timed.mark_owned(format!("vendor {} (test payment)", vendor.name));
let paid = match paid {
Ok(Some(p)) => p,
Ok(None) => {
return Err(Status::unimplemented(format!(
"vendor {:?} cannot pay a signup without a person (it has no test payment method)",
vendor.name
)))
}
Err(e) => {
return Err(Status::failed_precondition(format!(
"vendor (test payment): {e}"
)))
}
};
timed.outcome = "test paid";
if let Some(hold) = hold {
hold.minted(vendor.name, &paid.session);
}
// ★ A33-2: the session the vendor's test card paid is recorded as this box's own, with its
// welcome page, exactly as a buyer's minted checkout is (ST-7) — the one settle path below
// then reads it as a buyer's.
s.signups
.record_minted(vendor.name, &paid.session, estate)
.map_err(store_err)?;
s.signups
.record_welcome(vendor.name, &paid.session, success_url)
.map_err(store_err)?;
// The one settle path every verified webhook takes: kept first — with the site the
// press named, so the account is made THERE (ST-8) — then the sink.
s.signups
.record_pending_on(&paid, s.now() as i64, estate)
.map_err(store_err)?;
let decided = crate::signup::settle(s, &paid);
eprintln!("monetize-server: a TEST signup {}:{} for {:?} was paid with the vendor's test payment method: {decided:?}", paid.vendor, paid.session, paid.username);
Ok(pb::StartSignupResponse {
url: success_url.replace("{session}", &paid.session),
session: paid.session,
})
}
/// **The vendor a console's signup row is kept under.** A console names the vendor KIND
/// (`stripe`, gunnar-ui `auth::signup`); on a box selling one product for several sites the
/// webhook keeps the paid row under the calling site's INSTANCE (`stripe/gunnar-test2`,
/// `monetize_pay::instance`). The row under the name asked wins; else the calling estate's
/// own instance of that kind — never another site's (i54a, 2026-10-12: test2's welcome page
/// polled `stripe` and read UNKNOWN for ever).
fn row_vendor(s: &AppState, named: &str, session: &str, estate: Option<&str>) -> String {
// ★ A32-15: a site names no vendor (the shop link's vendor is empty): the calling estate's
// own vendor instance is the one its sign-ups were minted at.
if named.trim().is_empty() && !session.starts_with(crate::signup::ACCOUNT_SESSION_PREFIX) {
if let Some(v) = estate.and_then(|e| s.registry.estate_vendors().get(e).cloned()) {
return v;
}
}
let direct = crate::signup::vendor_of(named, session);
if direct != named || named.contains('/') {
return direct.to_owned();
}
match estate.and_then(|e| s.registry.instance_of_kind(e, named)) {
Some(instance) if !matches!(s.signups.get(named, session), Ok(Some(_))) => instance,
_ => named.to_owned(),
}
}
#[tonic::async_trait]
impl Signup for SignupService {
/// Read-only about the store and safe to poll every 2 s — with one
/// deliberate side effect, spelled out in [`crate::signup::status_with_retry`]:
/// a PENDING signup is pushed at the sink again before it is answered, so a
/// customer watching the page is not left waiting for the poller's tick
/// (which also retries it, for the customer who closed the tab —
/// `crate::signup::settle_pending`). A poll of a decided signup reads one
/// row and calls nobody.
async fn status(
&self,
req: Request<pb::SignupStatusRequest>,
) -> Result<Response<pb::SignupStatusResponse>, Status> {
let estate = req
.extensions()
.get::<crate::auth::CallerEstate>()
.map(|e| e.0.clone());
let r = req.into_inner();
let out = blocking(&self.0, move |s| {
let vendor = row_vendor(s, &r.vendor, &r.session, estate.as_deref());
// ★ A card check this box minted for a free account (owner 2026-10-15).
// Kept under the vendor INSTANCE that minted it: the row's vendor, else the
// calling estate's own vendor (a console names only the kind).
let estate_vendor = estate
.as_deref()
.and_then(|e| s.registry.estate_vendors().get(e).cloned())
.filter(|v| *v != vendor);
for v in std::iter::once(vendor.clone()).chain(estate_vendor) {
if let Some(answer) = finish_card_check(s, &v, &r.session)? {
return Ok(answer);
}
}
// ★ A paid signup this box minted and holds no row for: the vendor is asked (B).
if let Some(answer) = finish_paid_signup(s, &vendor, &r.session)? {
return Ok(answer);
}
let found =
crate::signup::status_with_retry(s, &vendor, &r.session).map_err(store_err)?;
Ok(match found {
Some(rec) => status_response(&s.signups, &rec, s.now() as i64),
// Never an error: the page may well be polling before the
// webhook landed. See the module doc.
None => pb::SignupStatusResponse {
state: pb::signup_status_response::State::Unknown as i32,
..Default::default()
},
})
})
.await?;
Ok(Response::new(out))
}
async fn send_sign_in_again(
&self,
req: Request<pb::SignupSignInAgainRequest>,
) -> Result<Response<pb::SignupSignInAgainResponse>, Status> {
let estate = req
.extensions()
.get::<crate::auth::CallerEstate>()
.map(|e| e.0.clone());
let r = req.into_inner();
let product = product_named(&r.product, "SendSignInAgain")?;
let out = blocking(&self.0, move |s| {
send_sign_in_again(s, &product, &r.session, &r.welcome_url, estate.as_deref())
})
.await?;
Ok(Response::new(out))
}
/// **Asked before the card form, and it holds nothing.**
///
/// It is an oracle by nature — anybody who can call it can learn whether a
/// name is taken — and the mitigation is not here. This service is behind
/// the same bearer as every other one; the surface that answers a stranger
/// is the console's route in front of it, which carries a per-address budget
/// and is where a limit belongs, because the addresses are there and not
/// here.
///
/// What this DOES refuse to do is say more than yes or no. `reason` is the
/// sentence the customer reads, it never names another principal, and there
/// is no verb here that lists anything.
async fn check_name(
&self,
req: Request<pb::CheckNameRequest>,
) -> Result<Response<pb::CheckNameResponse>, Status> {
let estate = req
.extensions()
.get::<crate::auth::CallerEstate>()
.map(|e| e.0.clone());
let r = req.into_inner();
let product = product_named(&r.product, "CheckName")?;
let out = blocking(&self.0, move |s| {
Ok(
match name_refusal(s, &product, &r.username, estate.as_deref())? {
None => pb::CheckNameResponse {
free: true,
reason: String::new(),
},
Some(reason) => pb::CheckNameResponse {
free: false,
reason,
},
},
)
})
.await?;
Ok(Response::new(out))
}
/// **Mint the checkout for a name that has already been checked.**
///
/// The name is checked AGAIN here, and that is not belt and braces: the
/// caller's check is a courtesy to a person typing, and this one is the
/// gate. It still closes nothing — a name free at this instant can be taken
/// while the customer types a card, which is what `Rename` is for — but it
/// means a console cannot mint a checkout for `admin` by skipping a step.
///
/// `UNIMPLEMENTED` when the vendor mints no signup page (`invoice`, and any
/// vendor written before this verb): that is the answer a console turns into
/// its old fixed buy link, which is why it must be distinguishable from a
/// refusal.
async fn start(
&self,
req: Request<pb::StartSignupRequest>,
) -> Result<Response<pb::StartSignupResponse>, Status> {
let estate = req
.extensions()
.get::<crate::auth::CallerEstate>()
.map(|e| e.0.clone());
let r = req.into_inner();
let out = blocking(&self.0, move |s| {
start_signup(s, &r, PayBy::Checkout, estate.as_deref())
})
.await?;
Ok(Response::new(out))
}
/// **The race CheckName cannot close, settled without a refund.**
///
/// See [`crate::signup::rename`]. The refusals are kept apart on the wire
/// because a page answers each of them differently: `NOT_FOUND` is a session
/// nobody has heard of, `FAILED_PRECONDITION` is a record a name cannot fix
/// (and the message is the original refusal, so the page can show it),
/// `INVALID_ARGUMENT` is the new name being refused in turn — which is the
/// ordinary case, and the page simply asks again — and `UNAVAILABLE` is
/// ours, where the customer waits rather than retypes.
/// **What a new account costs, before anybody types a name** (the sign-up
/// page, 2026-10-07). A read: no name, no vendor, no sink call. Where no
/// account can be made, UNIMPLEMENTED — as `Start` answers, so a console
/// offers nothing it could not finish.
async fn plans(
&self,
req: Request<pb::SignupPlansRequest>,
) -> Result<Response<pb::SignupPlansResponse>, Status> {
let r = req.into_inner();
let s = &self.0;
let product = product_named(&r.product, "Plans")?;
if !s.signup_sink.creates_accounts_for(&product) {
return Err(no_sink(&product));
}
let market = match monetize_proto::Market::try_from(r.market) {
Ok(monetize_proto::Market::Se) => Some(monetize_pay::Market::Se),
Ok(monetize_proto::Market::Fi) => Some(monetize_pay::Market::Fi),
Ok(monetize_proto::Market::Dk) => Some(monetize_pay::Market::Dk),
_ => None,
};
let default_market = s
.terms
.read()
.unwrap_or_else(|e| e.into_inner())
.default_market;
// ★ Room by the rule (round 5.3.1, owner 2026-10-08: ONE bar at sign-up
// too, "1 month" at 10 GiB and "12 months" from 10 to 1000 GiB): where
// this deployment prices plans, a new account is offered THEM, priced
// now in its market, each under its priced id.
let in_market = market.unwrap_or(default_market);
if let Some(rooms) = crate::plans::signup_rooms(s, &product, in_market) {
let plans = rooms
.into_iter()
.map(|room| pb::SignupPlanOffer {
id: room.id,
name: room.name,
resources_words: format!(
"{} of room, kept on the server and its replica",
room_words(room.plan.cap_bytes)
),
price_words: format!(
"{} {}",
monetize_pay::Money {
minor: room.plan.amount_minor,
currency: room.plan.currency
}
.in_words(),
period_words(room.plan.period_days)
),
cap_bytes: room.plan.cap_bytes,
period_days: room.plan.period_days,
})
.collect();
return Ok(Response::new(pb::SignupPlansResponse {
plans,
market: crate::convert::market_to_pb(in_market) as i32,
}));
}
let table = s.signup_plans.snapshot_of(&product);
// ONE market, and its name: the page sends it back with the order.
let one = market_on_offer(&table, &product, market, default_market);
let plans = plans_on_offer(&table, &product, market, default_market)
.into_iter()
.map(|p| pb::SignupPlanOffer {
id: p.id,
name: p.name,
resources_words: p.resources_words,
price_words: p.price_words,
cap_bytes: p.cap_bytes,
period_days: p.period_days,
})
.collect();
let market = one
.map(|m| crate::convert::market_to_pb(m) as i32)
.unwrap_or_default();
Ok(Response::new(pb::SignupPlansResponse { plans, market }))
}
async fn rename(
&self,
req: Request<pb::RenameSignupRequest>,
) -> Result<Response<pb::RenameSignupResponse>, Status> {
let estate = req
.extensions()
.get::<crate::auth::CallerEstate>()
.map(|e| e.0.clone());
let r = req.into_inner();
let out = blocking(&self.0, move |s| {
use crate::signup::RenameRefusal;
let vendor = row_vendor(s, &r.vendor, &r.session, estate.as_deref());
match crate::signup::rename(s, &vendor, &r.session, &r.username) {
Ok(rec) => Ok(pb::RenameSignupResponse {
status: Some(status_response(&s.signups, &rec, s.now() as i64)),
}),
Err(RenameRefusal::Unknown) => Err(Status::not_found(
"no signup here has that checkout session",
)),
Err(RenameRefusal::NotRenameable(why)) => Err(Status::failed_precondition(why)),
Err(RenameRefusal::Name(why)) => Err(Status::invalid_argument(why)),
// Money has moved here, so the sentence says they need not pay again.
Err(RenameRefusal::Unavailable(why)) => Err(unavailable_for_customer(
"rename",
&why,
crate::signup::words::NOT_ANSWERING_PAID,
)),
}
})
.await?;
Ok(Response::new(out))
}
}
#[tonic::async_trait]
impl SignupAdmin for SignupAdminService {
async fn list(
&self,
req: Request<pb::ListSignupsRequest>,
) -> Result<Response<pb::ListSignupsResponse>, Status> {
let limit = req.into_inner().limit as usize;
let rows = blocking(&self.0, move |s| {
let rows = s.signups.list(limit).map_err(store_err)?;
Ok(rows
.iter()
.map(|rec| row_to_pb(&s.signups, rec))
.collect::<Vec<_>>())
})
.await?;
Ok(Response::new(pb::ListSignupsResponse { signups: rows }))
}
/// **The owed-back queue.** See [`crate::signup::SignupStore::owed_back`]:
/// every payment taken for an account that was never created, oldest first,
/// UNCAPPED. `List`'s ceiling is a console page's and this is not a page —
/// a deployment with six hundred refusals owes six hundred people.
async fn list_owed_back(
&self,
_req: Request<pb::ListOwedBackRequest>,
) -> Result<Response<pb::ListOwedBackResponse>, Status> {
let rows = blocking(&self.0, move |s| {
let rows = s.signups.owed_back().map_err(store_err)?;
Ok(rows
.iter()
.map(|rec| row_to_pb(&s.signups, rec))
.collect::<Vec<_>>())
})
.await?;
Ok(Response::new(pb::ListOwedBackResponse {
total: rows.len() as u64,
signups: rows,
}))
}
/// See [`crate::signup::resend_welcome`]. The codes are the proto's.
async fn resend_welcome(
&self,
req: Request<pb::ResendWelcomeRequest>,
) -> Result<Response<pb::ResendWelcomeResponse>, Status> {
let r = req.into_inner();
let outcome = blocking(&self.0, move |s| {
use crate::signup::ResendRefusal;
crate::signup::resend_welcome(s, &r.vendor, &r.session, r.force).map_err(|e| match e {
ResendRefusal::Unknown => {
Status::not_found(format!("no signup {}:{} in the store", r.vendor, r.session))
}
ResendRefusal::NotSendable(why) => Status::failed_precondition(why),
ResendRefusal::Unavailable(why) => Status::unavailable(why),
})
})
.await?;
Ok(Response::new(pb::ResendWelcomeResponse { outcome }))
}
/// **Give a refused signup's money back.** See [`crate::signup::refund`];
/// OPERATOR BEARER ONLY ([`operator_only`]). The codes are the proto's.
async fn refund(
&self,
req: Request<pb::RefundSignupRequest>,
) -> Result<Response<pb::RefundSignupResponse>, Status> {
operator_only(&req, "Refund")?;
let r = req.into_inner();
let out = blocking(&self.0, move |s| {
// A session that is no signup may be an ORDER's (g-refund): the
// same verb, the same operator bearer, written in the ledger.
let out = match crate::signup::refund(
s,
&r.vendor,
&r.session,
r.amount_minor,
&r.reason,
r.force,
) {
Err(crate::signup::RefundRefusal::Unknown) => crate::reversal::refund_order(
s,
&r.vendor,
&r.session,
r.amount_minor,
&r.reason,
),
other => other,
}
.map_err(|e| refund_refusal(&r.vendor, &r.session, e))?;
Ok(refund_to_pb(&r.session, &out))
})
.await?;
Ok(Response::new(out))
}
/// **Close out a row refunded by hand.** See
/// [`crate::signup::settle_by_hand`]; OPERATOR BEARER ONLY.
async fn settle(
&self,
req: Request<pb::SettleSignupRequest>,
) -> Result<Response<pb::RefundSignupResponse>, Status> {
operator_only(&req, "Settle")?;
let r = req.into_inner();
let out = blocking(&self.0, move |s| {
let out = crate::signup::settle_by_hand(s, &r.vendor, &r.session, &r.note)
.map_err(|e| refund_refusal(&r.vendor, &r.session, e))?;
Ok(refund_to_pb(&r.session, &out))
})
.await?;
Ok(Response::new(out))
}
}
/// **The money verbs take the OPERATOR bearer, never the service one**
/// (g-refund-verb). The service bearer is held by the console backends —
/// gunnar-ui's customer-facing BFF among them — and anything holding it could
/// otherwise give any payment back. [`crate::auth::signup_admin_interceptor`]
/// marks a call that carried the operator bearer; everything else is refused
/// here, whichever bearer let it past the door.
fn operator_only<T>(req: &Request<T>, verb: &str) -> Result<(), Status> {
if req.extensions().get::<crate::auth::Operator>().is_some() {
return Ok(());
}
Err(Status::permission_denied(format!(
"SignupAdmin.{verb} takes the OPERATOR bearer ({} or <data_dir>/{}), not the service bearer; a server with none configured refuses it to everybody",
crate::config::OPERATOR_TOKEN_ENV,
crate::config::OPERATOR_TOKEN_FILE
)))
}
fn refund_refusal(vendor: &str, session: &str, e: crate::signup::RefundRefusal) -> Status {
use crate::signup::RefundRefusal as R;
match e {
R::Unknown => Status::not_found(format!(
"no signup and no order {vendor}:{session} in this deployment"
)),
R::NotRefundable(why) => Status::failed_precondition(why),
R::Invalid(why) => Status::invalid_argument(why),
R::Unavailable(why) => Status::unavailable(why),
}
}
/// The wire state of a refund row; `UNSPECIFIED` is "none recorded".
fn refund_state_of(r: Option<&crate::signup::SignupRefund>) -> pb::RefundState {
use crate::signup::SignupRefund as F;
match r {
None => pb::RefundState::Unspecified,
Some(F::Requested { .. }) => pb::RefundState::Requested,
Some(F::Done { .. }) => pb::RefundState::Done,
Some(F::SettledByHand { .. }) => pb::RefundState::SettledByHand,
}
}
fn refund_to_pb(session: &str, r: &crate::signup::SignupRefund) -> pb::RefundSignupResponse {
use crate::signup::SignupRefund as F;
let state = refund_state_of(Some(r)) as i32;
match r {
F::Requested {
amount,
reason,
at_unix_ms,
..
} => pb::RefundSignupResponse {
// Not yet confirmed; the key it is (being) sent under.
refund_id: crate::signup::refund_key(session, amount.minor),
amount: Some(crate::convert::money_to_pb(*amount)),
state,
reason: reason.clone(),
requested_at_unix_ms: *at_unix_ms,
done_at_unix_ms: 0,
},
F::Done {
refund_id,
amount,
reason,
requested_at_unix_ms,
at_unix_ms,
..
} => pb::RefundSignupResponse {
refund_id: refund_id.clone(),
amount: Some(crate::convert::money_to_pb(*amount)),
state,
reason: reason.clone(),
requested_at_unix_ms: *requested_at_unix_ms,
done_at_unix_ms: *at_unix_ms,
},
F::SettledByHand { note, at_unix_ms } => pb::RefundSignupResponse {
refund_id: String::new(),
amount: None,
state,
reason: note.clone(),
requested_at_unix_ms: 0,
done_at_unix_ms: *at_unix_ms,
},
}
}
/// The wire spelling of [`crate::signup::Owed`]. `UNSPECIFIED` for a row that
/// is not a refusal, which is what proto3's zero value means here — exactly as
/// [`refusal_of`] uses it.
fn owed_to_pb(owed: Option<crate::signup::Owed>) -> pb::signup_row::Owed {
use crate::signup::Owed as O;
use pb::signup_row::Owed as W;
match owed {
None => W::Unspecified,
Some(O::Unrecorded) => W::Unrecorded,
Some(O::Refund) => W::Refund,
Some(O::RefundAndTheNameMayBeSpent) => W::RefundAndTheNameMayBeSpent,
Some(O::RenameOrRefund) => W::RenameOrRefund,
}
}
/// One store row as the console reads it. The refusal reason is carried
/// verbatim: it is the operator's answer to "why did this customer's money
/// arrive and their account not". A PENDING row carries the sink's last "not
/// now" in the same field — the clock skew, the timeout — which is the one
/// place that sentence is allowed to be read.
fn row_to_pb(store: &crate::signup::SignupStore, rec: &SignupRecord) -> pb::SignupRow {
let (username, namespace, reason) = match &rec.state {
SignupState::Pending => (
rec.signup.username.clone(),
String::new(),
store
.waiting_on(&rec.signup.vendor, &rec.signup.session)
.unwrap_or_default(),
),
SignupState::Created {
namespace,
username,
..
} => (username.clone(), namespace.clone(), String::new()),
// A refused row still shows what the customer ASKED to be called —
// "the name was rejected" is unreadable without the name.
SignupState::Refused { reason, .. } => (
rec.signup.username.clone(),
String::new(),
crate::signup::legacy_price_refusal(reason).unwrap_or_else(|| reason.clone()),
),
};
// The money's own field, and the sentence that goes with it. Built from the
// record and not from `reason`: a console that had to read prose to know
// whether a name was spent would be one rewording away from being wrong.
let owed = rec.state.owed();
pb::SignupRow {
owed: owed_to_pb(owed) as i32,
remedy: owed.map(|o| o.remedy().to_owned()).unwrap_or_default(),
vendor: rec.signup.vendor.clone(),
session: rec.signup.session.clone(),
state: state_of(rec) as i32,
username,
namespace,
reason,
product: rec.signup.product.clone(),
plan: rec.signup.plan.clone(),
amount: Some(crate::convert::money_to_pb(rec.signup.amount)),
paid_at_unix_ms: rec.signup.paid_at_unix_ms,
recorded_at_unix_ms: rec.recorded_at_unix_ms,
decided_at_unix_ms: rec.state.decided_at_unix_ms(),
email: rec.signup.email.clone(),
livemode: monetize_pay::livemode_of(&rec.signup.vendor, &rec.signup.session),
refund: refund_state_of(
store
.refund_of(&rec.signup.vendor, &rec.signup.session)
.ok()
.flatten()
.as_ref(),
) as i32,
}
}
/// ★ g-currencies (money 2026-09-28): the Danish company sells the signup in
/// the customer's own currency — one plan per market, and a request that
/// names no plan but names a MARKET gets that market's plan. Two plans and
/// neither a plan nor a market is still a refusal that lists them.
#[cfg(test)]
mod plan_by_market_tests {
use super::*;
use monetize_pay::{Currency, Market};
fn plans() -> std::collections::BTreeMap<String, crate::config::SignupPlan> {
let p = |minor, currency| crate::config::SignupPlan {
amount_minor: minor,
currency,
cap_bytes: 10 << 30,
period_days: 30,
};
[
("gunnar/10gb-1m".to_string(), p(10_000, Currency::Sek)),
("gunnar/10gb-1m-fi".to_string(), p(870, Currency::Eur)),
("gunnar/10gb-1m-dk".to_string(), p(7_500, Currency::Dkk)),
]
.into()
}
#[test]
fn a_market_picks_its_own_plan_and_no_market_with_several_plans_is_refused() {
let ps = plans();
assert_eq!(
plan_for_market(&ps, "", Some(Market::Dk), Market::Se)
.unwrap()
.0,
"gunnar/10gb-1m-dk"
);
assert_eq!(
plan_for_market(&ps, "", Some(Market::Fi), Market::Se)
.unwrap()
.1
.currency,
Currency::Eur
);
assert_eq!(
plan_for_market(&ps, "", Some(Market::Se), Market::Se)
.unwrap()
.0,
"gunnar/10gb-1m"
);
assert_eq!(
plan_for_market(&ps, "gunnar/10gb-1m-fi", Some(Market::Dk), Market::Se)
.unwrap()
.0,
"gunnar/10gb-1m-fi",
"a named plan wins"
);
let one: std::collections::BTreeMap<_, _> = plans().into_iter().take(1).collect();
assert_eq!(
plan_for_market(&one, "", Some(Market::Dk), Market::Se)
.unwrap()
.0,
"gunnar/10gb-1m",
"one plan is THE plan, whatever the market"
);
}
/// ★ release-3 (MEASURED on the dwarf mock 2026-09-29, install-orig-8): a
/// signup naming no plan and no market — the functest, and every console
/// whose browser language names no market (en-US) — was refused 409 once
/// money sold a plan per market. With exactly ONE plan per market, no market
/// is the DEFAULT market's plan; two plans in one market stay a named refusal.
#[test]
fn no_market_takes_the_default_markets_plan_when_each_market_has_one() {
let ps = plans();
assert_eq!(
plan_for_market(&ps, "", None, Market::Se).unwrap().0,
"gunnar/10gb-1m",
"no market: the default market's one plan"
);
assert_eq!(
plan_for_market(&ps, "", None, Market::Dk).unwrap().0,
"gunnar/10gb-1m-dk",
"the deployment's default market decides"
);
let mut two_in_se = plans();
let p = two_in_se["gunnar/10gb-1m"];
two_in_se.insert("gunnar/50gb-1m".into(), p);
let refused = plan_for_market(&two_in_se, "", None, Market::Se).unwrap_err();
assert!(
refused.message().contains("named none"),
"two plans in one market and no name: refused, never a pick: {refused:?}"
);
assert_eq!(
plan_for_market(&two_in_se, "", Some(Market::Dk), Market::Se)
.unwrap()
.0,
"gunnar/10gb-1m-dk",
"a market with ONE plan still picks it"
);
}
}
#[cfg(test)]
mod plans_on_offer_tests {
//! **`Signup.Plans`** (the sign-up page, 2026-10-07): what a new account
//! costs, before anybody types a name — the page shows these words and
//! writes no price of its own.
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use monetize_pay::{Currency, Market};
use super::*;
use crate::signup::tests::{state_with, CountingSink};
fn plans() -> std::collections::BTreeMap<String, crate::config::SignupPlan> {
let p = |minor, currency, gib: u64, days| crate::config::SignupPlan {
amount_minor: minor,
currency,
cap_bytes: gib << 30,
period_days: days,
};
[
(
"gunnar/10gb-1m".to_string(),
p(10_000, Currency::Sek, 10, 30),
),
(
"gunnar/50gb-1m".to_string(),
p(40_000, Currency::Sek, 50, 30),
),
(
"gunnar/10gb-1m-dk".to_string(),
p(7_500, Currency::Dkk, 10, 30),
),
(
"gunnar/100gb-1y".to_string(),
p(400_000, Currency::Sek, 100, 365),
),
(
"holger/10gb-1m".to_string(),
p(9_900, Currency::Sek, 10, 30),
),
]
.into()
}
/// ★ **The plans on offer, smallest first, each as words** — the
/// product's own, in the customer's market where it has any.
#[test]
fn the_plans_on_offer_are_the_markets_own_smallest_first_as_words() {
let shown = plans_on_offer(&plans(), "gunnar", Some(Market::Se), Market::Se);
assert_eq!(
shown.iter().map(|o| o.id.as_str()).collect::<Vec<_>>(),
["gunnar/10gb-1m", "gunnar/50gb-1m", "gunnar/100gb-1y"]
);
let ten = &shown[0];
assert_eq!(
(ten.name.as_str(), ten.price_words.as_str()),
("10 GB", "100.00 SEK a month")
);
assert_eq!(
ten.resources_words,
"10 GB of room, kept on the server and its replica"
);
assert_eq!(ten.cap_bytes, 10 << 30);
assert_eq!(shown[2].price_words, "4000.00 SEK a year");
// A Dane sees the Danish plan; holger's plans are holger's.
let dk = plans_on_offer(&plans(), "gunnar", Some(Market::Dk), Market::Se);
assert_eq!(
dk.iter().map(|o| o.id.as_str()).collect::<Vec<_>>(),
["gunnar/10gb-1m-dk"]
);
assert!(plans_on_offer(&plans(), "", None, Market::Se)
.iter()
.all(|o| o.id.starts_with("gunnar/") || o.id.starts_with("holger/")));
}
/// The kit's own `[signups.<product>.plans]` (private-gunnar-ops deploy/monetize.toml.tmpl):
/// ONE 10 GB plan per market, in its currency, and the account at the shop.
fn the_kits_plans() -> std::collections::BTreeMap<String, crate::config::SignupPlan> {
let p = |minor, currency, gib: u64, days| crate::config::SignupPlan {
amount_minor: minor,
currency,
cap_bytes: gib << 30,
period_days: days,
};
[
(
"gunnar/10gb-1m".to_string(),
p(11_000, Currency::Sek, 10, 30),
),
(
"gunnar/10gb-1m-fi".to_string(),
p(1_000, Currency::Eur, 10, 30),
),
(
"gunnar/10gb-1m-dk".to_string(),
p(7_460, Currency::Dkk, 10, 30),
),
("gunnar/account".to_string(), p(0, Currency::Sek, 0, 0)),
]
.into()
}
fn currencies(shown: &[PlanOnOffer]) -> std::collections::BTreeSet<String> {
shown
.iter()
.map(|o| {
o.price_words
.split_whitespace()
.nth(1)
.unwrap_or_default()
.to_owned()
})
.collect()
}
/// ★ **ONE market's plans, never a mix of currencies** (test2, owner
/// 2026-10-08: on `/signup?plan=10gb` every press of + changed the
/// CURRENCY — `Signup.Plans` answered every market's plans to a browser
/// whose language names no market, and the page stepped 110 SEK → 10 EUR →
/// 74.60 DKK). No market, or one with nothing on offer: the deployment's
/// DEFAULT market's, and the answer names the market it is.
#[test]
fn the_plans_on_offer_are_one_markets_never_a_mix_of_currencies() {
let kit = the_kits_plans();
let none = plans_on_offer(&kit, "gunnar", None, Market::Se);
assert_eq!(
none.iter().map(|o| o.id.as_str()).collect::<Vec<_>>(),
["gunnar/10gb-1m"],
"no market: the default market's one plan, not three currencies"
);
assert_eq!(
market_on_offer(&kit, "gunnar", None, Market::Se),
Some(Market::Se)
);
let dk_home = plans_on_offer(&kit, "gunnar", None, Market::Dk);
assert_eq!(
dk_home.iter().map(|o| o.id.as_str()).collect::<Vec<_>>(),
["gunnar/10gb-1m-dk"],
"the deployment's default market decides"
);
assert_eq!(
plans_on_offer(&kit, "gunnar", Some(Market::Fi), Market::Se)[0].price_words,
"10.00 EUR a month",
"a Finn sees the Finnish plan"
);
// A market with nothing on offer is the default market's, whole.
let fi = plans_on_offer(&plans(), "gunnar", Some(Market::Fi), Market::Se);
assert_eq!(
fi.iter().map(|o| o.id.as_str()).collect::<Vec<_>>(),
["gunnar/10gb-1m", "gunnar/50gb-1m", "gunnar/100gb-1y"]
);
assert_eq!(
market_on_offer(&plans(), "gunnar", Some(Market::Fi), Market::Se),
Some(Market::Se)
);
// A default market with nothing on offer: the first market that has some.
let only_dk: std::collections::BTreeMap<_, _> = kit
.iter()
.filter(|(id, _)| id.ends_with("-dk"))
.map(|(k, v)| (k.clone(), *v))
.collect();
assert_eq!(
market_on_offer(&only_dk, "gunnar", None, Market::Se),
Some(Market::Dk)
);
for (market, default) in [
(None, Market::Se),
(Some(Market::Fi), Market::Se),
(Some(Market::Dk), Market::Se),
(None, Market::Dk),
] {
for table in [kit.clone(), plans()] {
let shown = plans_on_offer(&table, "gunnar", market, default);
assert!(
currencies(&shown).len() <= 1,
"{market:?}/{default:?}: one currency, never {:?}",
currencies(&shown)
);
}
}
assert_eq!(
market_on_offer(&Default::default(), "gunnar", None, Market::Se),
None,
"nothing on offer is no market"
);
}
/// ★ **Over the wire: one market, and its name** — the answer to a page
/// that names no market is the default market's plans, said as `SE`.
#[tokio::test]
async fn the_plans_answer_one_market_over_the_wire_and_name_it() {
use monetize_proto::signup_server::Signup as _;
let state = state_with(CountingSink::creating(), Arc::new(AtomicU64::new(5)));
state.signup_plans.set(the_kits_plans(), Default::default());
let out = SignupService(state.clone())
.plans(Request::new(pb::SignupPlansRequest {
product: "gunnar".into(),
market: 0,
}))
.await
.unwrap()
.into_inner();
assert_eq!(
out.plans
.iter()
.map(|p| p.price_words.as_str())
.collect::<Vec<_>>(),
["110.00 SEK a month"],
"{out:?}"
);
assert_eq!(
out.market,
pb::Market::Se as i32,
"the answer names its market"
);
let dk = SignupService(state)
.plans(Request::new(pb::SignupPlansRequest {
product: "gunnar".into(),
market: pb::Market::Dk as i32,
}))
.await
.unwrap()
.into_inner();
assert_eq!(
(dk.plans.len(), dk.plans[0].id.as_str(), dk.market),
(1, "gunnar/10gb-1m-dk", pb::Market::Dk as i32)
);
}
/// **Over the wire**, and refused by name where no account can be made.
#[tokio::test]
async fn the_plans_answer_over_the_wire_and_nothing_is_offered_where_no_account_can_be_made() {
use monetize_proto::signup_server::Signup as _;
let state = state_with(CountingSink::creating(), Arc::new(AtomicU64::new(5)));
let out = SignupService(state)
.plans(Request::new(pb::SignupPlansRequest {
product: "gunnar".into(),
market: pb::Market::Se as i32,
}))
.await
.unwrap()
.into_inner();
assert_eq!(out.plans.len(), 1);
assert_eq!(
(
out.plans[0].id.as_str(),
out.plans[0].name.as_str(),
out.plans[0].price_words.as_str()
),
("gunnar/10gb-1m", "10 GB", "100.00 SEK a month")
);
// No sink that makes an account: none is offered.
let mut bare = crate::state::AppState::in_memory(crate::registry::Registry::new());
bare.signup_sink = Arc::new(crate::signup::NoSink);
bare.signup_plans = plans().into();
let refused = SignupService(Arc::new(bare))
.plans(Request::new(pb::SignupPlansRequest {
product: "gunnar".into(),
market: 0,
}))
.await
.unwrap_err();
assert_eq!(refused.code(), tonic::Code::Unimplemented, "{refused:?}");
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use monetize_pay::signup::SignupError;
use super::*;
use crate::signup::tests::{a_signup, state_with, CountingSink};
use crate::signup::{settle, words};
/// ★ **One line per `Signup.Start`, with the total and every stage** — test2
/// 2026-09-30 had 25 s with NOTHING logged between two lines, and the time
/// could not be put anywhere. Past [`START_BUDGET`] it says FAILED, which the
/// front raises to WARN.
#[test]
fn a_signup_start_says_its_total_and_every_stage_once() {
static SAID: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
{
let mut t = StartTimings::saying("henrik", |l| SAID.lock().unwrap().push(l.to_owned()));
t.mark("name");
t.mark("capacity");
t.mark("sold-capacity");
t.mark_owned("vendor stripe".into());
t.outcome = "minted";
}
let said = SAID.lock().unwrap().clone();
assert_eq!(said.len(), 1, "{said:?}");
let l = &said[0];
assert!(l.contains("signup start \"henrik\": minted in "), "{l}");
for stage in ["name ", "capacity ", "sold-capacity ", "vendor stripe "] {
assert!(l.contains(stage), "{stage:?} missing: {l}");
}
assert!(!l.contains("FAILED"), "a fast start is not a warning: {l}");
let t = StartTimings::saying("olle", |_| {});
let slow = t.line(START_BUDGET + std::time::Duration::from_millis(1));
assert!(
slow.contains("not minted in ") && slow.contains("FAILED"),
"{slow}"
);
}
/// **What `Signup.Status` puts on the wire for a signup pending on a clock
/// skew: the settling sentence, and none of the skew.** The operator's
/// `SignupAdmin.List` row for the same session carries gunnar's words.
#[test]
fn a_skew_pending_status_carries_customer_words_and_the_admin_row_carries_the_skew() {
let skew = "gunnar Access.ListPrincipals -> Unauthenticated: the credential is 412000 ms away from this server's clock; the window is ±300000 ms";
let state = state_with(
CountingSink::answering(Err(SignupError::Unavailable(skew.into()))),
Arc::new(AtomicU64::new(1_000)),
);
settle(&state, &a_signup("cs_skew", "bob"));
let rec = state.signups.get("mockpay", "cs_skew").unwrap().unwrap();
let wire = status_response(&state.signups, &rec, state.now() as i64);
assert_eq!(wire.state(), pb::signup_status_response::State::Pending);
assert_eq!(
wire.words,
words::BEING_CREATED,
"a skew is a settle, never maintenance (PAID-WORDS)"
);
let customer = format!("{} {}", wire.words, wire.reason).to_lowercase();
for banned in ["clock", "skew", "credential", "unauthenticated", "ms away"] {
assert!(
!customer.contains(banned),
"the customer reads {banned:?}: {customer}"
);
}
let row = row_to_pb(&state.signups, &rec);
assert_eq!(row.reason, skew, "the operator reads what gunnar said");
// A signup still being tried owes nobody anything YET: it may still
// become an account.
assert_eq!(row.owed(), pb::signup_row::Owed::Unspecified);
assert!(row.remedy.is_empty());
}
/// (r) **`SignupAdmin.ListOwedBack` serves the queue, with the job on each
/// row** — money taken, account not created, and the sentence that says what
/// a human must do about it.
///
/// The count on the response is the same number the poll line prints, so a
/// console and a journal cannot disagree about how many people are waiting
/// for their money.
#[tokio::test]
async fn list_owed_back_serves_the_payments_no_account_was_made_for() {
use monetize_proto::signup_admin_server::SignupAdmin as _;
let state = state_with(
CountingSink::answering(Err(SignupError::Refused(
"Entitlement.Set: quota rejected".into(),
))),
Arc::new(AtomicU64::new(5)),
);
settle(&state, &crate::signup::tests::a_signup("cs_owed", "bob"));
let svc = SignupAdminService(state.clone());
let out = svc
.list_owed_back(Request::new(pb::ListOwedBackRequest {}))
.await
.unwrap()
.into_inner();
assert_eq!(out.total, 1);
let row = &out.signups[0];
assert_eq!(row.session, "cs_owed");
assert_eq!(row.owed(), pb::signup_row::Owed::RefundAndTheNameMayBeSpent);
assert!(
row.remedy.to_lowercase().contains("refund"),
"{}",
row.remedy
);
assert_eq!(
row.amount.as_ref().map(|m| m.minor),
Some(10_000),
"the queue carries the money, not only the name"
);
assert_eq!(
row.email, "bob@example.com",
"an operator who cannot see whom to pay back cannot pay anybody back"
);
// A CREATED signup is not on the queue and carries no remedy.
let created = state_with(CountingSink::creating(), Arc::new(AtomicU64::new(6)));
settle(&created, &crate::signup::tests::a_signup("cs_made", "bob"));
let out = SignupAdminService(created)
.list_owed_back(Request::new(pb::ListOwedBackRequest {}))
.await
.unwrap()
.into_inner();
assert_eq!((out.total, out.signups.len()), (0, 0));
}
// ── g-refund-verb (2026-09-28): the refund, by the operator, once ─────────
/// A deployment whose `mockpay` vendor is the testing double, so a refund
/// reaches a vendor that keeps Stripe's idempotency promise and records
/// every call it was sent.
fn refund_rig(
sink: Arc<dyn monetize_pay::signup::SignupSink>,
) -> (Arc<AppState>, crate::testing::CallbackPay) {
let pay = crate::testing::CallbackPay::new("whsec_test_only");
let mut state =
AppState::in_memory(crate::registry::Registry::new().sells("gunnar", pay.clone()));
state.signup_sink = sink;
state.signup_plans = crate::signup::tests::plans().into();
let tick = Arc::new(AtomicU64::new(1_788_436_900_000));
state.clock = Box::new(move || tick.load(std::sync::atomic::Ordering::SeqCst));
(Arc::new(state), pay)
}
/// A signup the CHECKS refused (a plan nobody sells): `Owed::Refund`,
/// money taken, nothing made.
fn refused_for_a_refund(state: &AppState, pay: &crate::testing::CallbackPay, session: &str) {
let mut s = a_signup(session, "bob");
s.plan = "gunnar/not-sold".into();
pay.took_signup(session);
let out = settle(state, &s);
assert!(
matches!(
out,
SignupState::Refused {
owed: crate::signup::Owed::Refund,
..
}
),
"{out:?}"
);
}
fn as_operator<T>(msg: T) -> Request<T> {
let mut r = Request::new(msg);
r.extensions_mut().insert(crate::auth::Operator);
r
}
fn refund_req(session: &str, amount_minor: u64) -> pb::RefundSignupRequest {
pb::RefundSignupRequest {
vendor: "mockpay".into(),
session: session.into(),
amount_minor,
reason: "S10: refused at signup".into(),
force: false,
}
}
/// (r) **g-refund-verb: a refused signup's money goes back ONCE, and the
/// alarm settles.** Before: `Owed::Refund` rows could only be refunded by
/// hand in the vendor's dashboard, and nothing could take one off the
/// owed-back queue, so `owed_back=` stayed red for ever.
///
/// `Refund` twice: exactly one vendor call, under the key Stripe keys a
/// refund by (`<session>:refund:<amount>`), the whole payment when the
/// amount is 0, the record DONE, the queue empty. And a request without the
/// operator bearer's mark is refused and calls nobody.
#[tokio::test]
async fn refund_gives_a_refused_signups_money_back_once_and_settles_the_owed_back_queue() {
use monetize_proto::signup_admin_server::SignupAdmin as _;
let (state, pay) = refund_rig(CountingSink::creating());
refused_for_a_refund(&state, &pay, "cs_owed");
let svc = SignupAdminService(state.clone());
// The service bearer's call carries no operator mark: refused, and the
// vendor is never asked.
let denied = svc
.refund(Request::new(refund_req("cs_owed", 0)))
.await
.unwrap_err();
assert_eq!(denied.code(), tonic::Code::PermissionDenied, "{denied:?}");
assert!(
pay.refund_calls().is_empty(),
"a refused caller reached the vendor"
);
let first = svc
.refund(as_operator(refund_req("cs_owed", 0)))
.await
.unwrap()
.into_inner();
assert_eq!(first.state(), pb::RefundState::Done);
assert_eq!(
first.amount.as_ref().map(|m| m.minor),
Some(10_000),
"0 is the whole payment"
);
assert_eq!(first.refund_id, "cs_owed:refund:10000");
assert!(first.done_at_unix_ms > 0);
let second = svc
.refund(as_operator(refund_req("cs_owed", 0)))
.await
.unwrap()
.into_inner();
assert_eq!(second, first, "the second call answers the first refund");
assert_eq!(
pay.refund_calls(),
vec!["cs_owed:refund:10000".to_string()],
"exactly one vendor refund call"
);
assert_eq!(pay.refunds_made(), 1);
assert!(
state.signups.owed_back().unwrap().is_empty(),
"a DONE refund is not owed back any more"
);
let out = svc
.list_owed_back(Request::new(pb::ListOwedBackRequest {}))
.await
.unwrap()
.into_inner();
assert_eq!((out.total, out.signups.len()), (0, 0));
// The row is still in the store, and says it was refunded.
let rows = svc
.list(Request::new(pb::ListSignupsRequest { limit: 0 }))
.await
.unwrap()
.into_inner()
.signups;
assert_eq!(rows[0].refund(), pb::RefundState::Done);
}
/// (r) **The crash between the vendor's yes and our record of it.** The
/// refund is REQUESTED, the vendor makes it, and the reply never arrives
/// (a lost connection; the process dying is the same state on disk). The
/// row stays on the queue as REQUESTED. The retry calls the vendor AGAIN
/// under the SAME key — so the vendor refunds nothing twice — and the
/// record completes.
#[tokio::test]
async fn a_refund_whose_reply_was_lost_is_finished_by_the_retry_under_the_same_key() {
use monetize_proto::signup_admin_server::SignupAdmin as _;
let (state, pay) = refund_rig(CountingSink::creating());
refused_for_a_refund(&state, &pay, "cs_lost");
let svc = SignupAdminService(state.clone());
pay.lose_next_refund_reply();
let lost = svc
.refund(as_operator(refund_req("cs_lost", 4_000)))
.await
.unwrap_err();
assert_eq!(lost.code(), tonic::Code::Unavailable, "{lost:?}");
assert_eq!(pay.refunds_made(), 1, "the vendor did refund");
let owed = svc
.list_owed_back(Request::new(pb::ListOwedBackRequest {}))
.await
.unwrap()
.into_inner();
assert_eq!(
owed.total, 1,
"REQUESTED is still owed: nobody heard the vendor say yes"
);
assert_eq!(owed.signups[0].refund(), pb::RefundState::Requested);
// The retry names 0 ("the whole"), and still sends the RECORDED amount:
// a different amount would be a different key, and a second refund.
let done = svc
.refund(as_operator(refund_req("cs_lost", 0)))
.await
.unwrap()
.into_inner();
assert_eq!(done.state(), pb::RefundState::Done);
assert_eq!(done.amount.as_ref().map(|m| m.minor), Some(4_000));
assert_eq!(
pay.refund_calls(),
vec!["cs_lost:refund:4000".to_string(); 2],
"the retry re-sent the same key"
);
assert_eq!(pay.refunds_made(), 1, "one refund at the vendor");
assert!(state.signups.owed_back().unwrap().is_empty());
}
/// (r) **What the verb refuses, by name.** A PENDING signup (it may still
/// become an account) even with `force`; a CREATED one without `force`;
/// more than was paid; a vendor this server does not have; a session
/// nobody has heard of. None of them reaches a vendor.
#[tokio::test]
async fn refund_refuses_what_is_not_owed_back_by_name() {
use monetize_proto::signup_admin_server::SignupAdmin as _;
let (state, pay) = refund_rig(CountingSink::creating());
let svc = SignupAdminService(state.clone());
refused_for_a_refund(&state, &pay, "cs_over");
let over = svc
.refund(as_operator(refund_req("cs_over", 10_001)))
.await
.unwrap_err();
assert_eq!(over.code(), tonic::Code::InvalidArgument, "{over:?}");
state
.signups
.record_pending(&a_signup("cs_pending", "carol"), 1)
.unwrap();
let pending = svc
.refund(as_operator(pb::RefundSignupRequest {
force: true,
..refund_req("cs_pending", 0)
}))
.await
.unwrap_err();
assert_eq!(
pending.code(),
tonic::Code::FailedPrecondition,
"{pending:?}"
);
pay.took_signup("cs_made");
settle(&state, &a_signup("cs_made", "bob"));
let made = svc
.refund(as_operator(refund_req("cs_made", 0)))
.await
.unwrap_err();
assert_eq!(made.code(), tonic::Code::FailedPrecondition, "{made:?}");
assert!(
made.message().contains("force"),
"the refusal names the way through: {}",
made.message()
);
let mut ghost = a_signup("cs_ghost", "dave");
ghost.vendor = "ghostpay".into();
ghost.plan = "gunnar/not-sold".into();
settle(&state, &ghost);
let ghosted = svc
.refund(as_operator(pb::RefundSignupRequest {
vendor: "ghostpay".into(),
..refund_req("cs_ghost", 0)
}))
.await
.unwrap_err();
assert_eq!(
ghosted.code(),
tonic::Code::FailedPrecondition,
"{ghosted:?}"
);
assert!(
ghosted.message().contains("ghostpay"),
"{}",
ghosted.message()
);
let unknown = svc
.refund(as_operator(refund_req("cs_nobody", 0)))
.await
.unwrap_err();
assert_eq!(unknown.code(), tonic::Code::NotFound, "{unknown:?}");
assert!(
pay.refund_calls().is_empty(),
"a refused refund called a vendor: {:?}",
pay.refund_calls()
);
assert_eq!(
state.signups.owed_back().unwrap().len(),
2,
"cs_over and cs_ghost are still owed"
);
}
/// (r) **`force` refunds a CREATED signup, and the tax export credits it
/// back.** The account stays (removing it is `Entitlement`'s, by hand);
/// what the customer got back is not a sale — a correction nets it out
/// (stinky row 17: the sale itself is never rewritten).
#[tokio::test]
async fn a_forced_refund_of_a_created_signup_is_credited_back_in_full() {
use monetize_proto::signup_admin_server::SignupAdmin as _;
let (state, pay) = refund_rig(CountingSink::creating());
pay.took_signup("cs_made");
settle(&state, &a_signup("cs_made", "bob"));
let books = state.books.read().unwrap().clone();
assert_eq!(
crate::signup::tax_sales(&state.signups, &books)
.unwrap()
.len(),
1
);
let svc = SignupAdminService(state.clone());
let done = svc
.refund(as_operator(pb::RefundSignupRequest {
force: true,
..refund_req("cs_made", 0)
}))
.await
.unwrap()
.into_inner();
assert_eq!(done.state(), pb::RefundState::Done);
let lines = crate::signup::tax_sales(&state.signups, &books).unwrap();
assert_eq!(
lines.iter().map(|s| s.credit).collect::<Vec<_>>(),
[false, true],
"the sale, then its credit: {lines:?}"
);
assert_eq!(
lines.iter().map(|s| s.signed_net().minor).sum::<i64>(),
0,
"a sale refunded in full nets to nothing"
);
assert!(
state.signups.owed_back().unwrap().is_empty(),
"a created signup was never owed back"
);
}
/// Stinky row 17 (VAT, 2026-10-10): **a forced refund a quarter after the
/// signup is a correction in the REFUND's quarter.** The signup's quarter is
/// a return that may already be filed; it must read the same after the
/// money goes back.
#[tokio::test]
async fn a_forced_refund_a_quarter_later_is_a_correction_there_and_the_signups_quarter_never_moves(
) {
use monetize::tax::{Amount, Declared, LedgerView, PeriodKind, TaxReport as _};
use monetize_pay::{Currency, Money};
use monetize_proto::signup_admin_server::SignupAdmin as _;
use std::sync::atomic::Ordering::SeqCst;
let pay = crate::testing::CallbackPay::new("whsec_test_only");
let mut state =
AppState::in_memory(crate::registry::Registry::new().sells("gunnar", pay.clone()));
state.signup_sink = CountingSink::creating();
state.signup_plans = crate::signup::tests::plans().into();
let tick = Arc::new(AtomicU64::new(1_788_436_900_000));
let clock = Arc::clone(&tick);
state.clock = Box::new(move || clock.load(SeqCst));
let state = Arc::new(state);
let mut paid = a_signup("cs_made", "bob");
paid.amount = Money {
minor: 12_500,
currency: Currency::Sek,
};
paid.tax = Money {
minor: 2_500,
currency: Currency::Sek,
};
pay.took_signup("cs_made");
assert!(matches!(
settle(&state, &paid),
crate::signup::SignupState::Created { .. }
));
let books = state.books.read().unwrap().clone();
let view = || LedgerView {
home_country: "SE".into(),
sales: crate::signup::tax_sales(&state.signups, &books).unwrap(),
};
let quarter_of = |at: u64| {
PeriodKind::Quarterly
.periods(2026)
.into_iter()
.find(|p| p.contains(at))
.unwrap()
};
let se = Declared {
country: "SE".into(),
basis_points: 2500,
period: PeriodKind::Quarterly,
currency: Some(Currency::Sek),
};
let box_of = |r: &monetize::tax::Report, id: &str| {
r.lines
.iter()
.find(|l| l.box_id == id)
.and_then(|l| l.amount)
.map(|a: Amount| a.minor)
};
let q_sale = quarter_of(paid.paid_at_unix_ms as u64);
let filed = se.report(&view(), &q_sale).unwrap();
assert_eq!(box_of(&filed, "sales"), Some(10_000));
let refund_at = q_sale.to_unix_ms + 10 * 86_400_000;
tick.store(refund_at, SeqCst);
let svc = SignupAdminService(state.clone());
let done = svc
.refund(as_operator(pb::RefundSignupRequest {
force: true,
..refund_req("cs_made", 5_000)
}))
.await
.unwrap()
.into_inner();
assert_eq!(done.state(), pb::RefundState::Done);
assert_eq!(
se.report(&view(), &q_sale).unwrap().lines,
filed.lines,
"the signup's filed quarter is untouched by a refund a quarter later"
);
let corrected = se.report(&view(), &quarter_of(refund_at)).unwrap();
assert_eq!(
box_of(&corrected, "sales"),
Some(-4_000),
"the net that went back, in the refund's quarter"
);
assert_eq!(
box_of(&corrected, "output-tax"),
Some(-1_000),
"and its tax"
);
}
/// (r) **A row refunded BY HAND is settled with the operator's note**, and
/// leaves the queue. It moves no money; a row with a refund already
/// recorded is not settled over it.
#[tokio::test]
async fn settle_closes_a_row_refunded_by_hand_and_moves_no_money() {
use monetize_proto::signup_admin_server::SignupAdmin as _;
let (state, pay) = refund_rig(CountingSink::creating());
refused_for_a_refund(&state, &pay, "cs_hand");
let svc = SignupAdminService(state.clone());
let settle_req = |note: &str| pb::SettleSignupRequest {
vendor: "mockpay".into(),
session: "cs_hand".into(),
note: note.into(),
};
assert_eq!(
svc.settle(Request::new(settle_req("re_123 by rickard")))
.await
.unwrap_err()
.code(),
tonic::Code::PermissionDenied
);
assert_eq!(
svc.settle(as_operator(settle_req("")))
.await
.unwrap_err()
.code(),
tonic::Code::InvalidArgument,
"a settle says what was done"
);
let out = svc
.settle(as_operator(settle_req("re_123 by rickard")))
.await
.unwrap()
.into_inner();
assert_eq!(out.state(), pb::RefundState::SettledByHand);
assert_eq!(out.reason, "re_123 by rickard");
assert!(state.signups.owed_back().unwrap().is_empty());
assert!(pay.refund_calls().is_empty(), "settling moves no money");
// And a settled row is not then refunded a second time by the verb.
assert_eq!(
svc.refund(as_operator(refund_req("cs_hand", 0)))
.await
.unwrap_err()
.code(),
tonic::Code::FailedPrecondition
);
assert!(pay.refund_calls().is_empty());
}
/// (r) **On the wire: the service bearer — the one the customer-facing
/// console backend holds — cannot refund.** `ListOwedBack` still takes it;
/// `Refund` answers PERMISSION_DENIED to it and takes only the operator's.
#[tokio::test]
async fn the_service_bearer_is_refused_on_refund_and_the_operator_bearer_is_not() {
let (state, pay) = refund_rig(CountingSink::creating());
refused_for_a_refund(&state, &pay, "cs_wire");
let server = crate::testing::TestServer::spawn_with_operator(
state.clone(),
"svc-bearer",
"op-bearer",
)
.await;
let channel = tonic::transport::Endpoint::from_shared(server.endpoint())
.unwrap()
.connect()
.await
.unwrap();
let mut client = monetize_proto::signup_admin_client::SignupAdminClient::new(channel);
let with = |bearer: &str, msg: pb::RefundSignupRequest| {
let mut r = Request::new(msg);
r.metadata_mut()
.insert("authorization", format!("Bearer {bearer}").parse().unwrap());
r
};
let mut list = Request::new(pb::ListOwedBackRequest {});
list.metadata_mut()
.insert("authorization", "Bearer svc-bearer".parse().unwrap());
assert_eq!(
client
.list_owed_back(list)
.await
.unwrap()
.into_inner()
.total,
1,
"the service bearer still reads the queue"
);
let denied = client
.refund(with("svc-bearer", refund_req("cs_wire", 0)))
.await
.unwrap_err();
assert_eq!(denied.code(), tonic::Code::PermissionDenied, "{denied:?}");
let stranger = client
.refund(with("nobody", refund_req("cs_wire", 0)))
.await
.unwrap_err();
assert_eq!(
stranger.code(),
tonic::Code::Unauthenticated,
"{stranger:?}"
);
assert!(pay.refund_calls().is_empty());
let done = client
.refund(with("op-bearer", refund_req("cs_wire", 0)))
.await
.unwrap()
.into_inner();
assert_eq!(done.state(), pb::RefundState::Done);
assert_eq!(pay.refund_calls().len(), 1);
server.stop().await;
}
}
#[cfg(test)]
mod site_tests {
use super::site_refusal;
/// ★ **A site's signup returns to that site** (owner 2026-10-11: two sites, one product,
/// one box): a success or cancel page on another host — another site, or a look-alike — is
/// refused by name; on the site's own https origin it passes.
#[test]
fn a_sites_signup_pages_are_on_that_site() {
let site = "test2.gunnar.rs";
assert_eq!(
site_refusal(
site,
"https://test2.gunnar.rs/welcome?session={session}",
"https://test2.gunnar.rs/signup"
),
None
);
assert_eq!(site_refusal(site, "", ""), None, "the vendor's own pages");
// WWW-CANON (owner 2026-10-30): the site is www.<site>; its console asks from that origin.
assert_eq!(
site_refusal(
site,
"https://www.test2.gunnar.rs/welcome?session={session}",
"https://www.test2.gunnar.rs/signup"
),
None,
"the site's www origin is the site"
);
for (ok, cancel) in [
("https://www.gunnar.rs/welcome", ""),
("https://www.test2.gunnar.rs.evil.example/welcome", ""),
("http://www.test2.gunnar.rs/welcome", ""),
("https://gunnar.rs/welcome", ""),
("https://test2.gunnar.rs.evil.example/welcome", ""),
("http://test2.gunnar.rs/welcome", ""),
("https://test2.gunnar.rs/w", "https://gunnar.rs/signup"),
] {
let e = site_refusal(site, ok, cancel).expect("another site's page");
assert!(e.contains(site), "{e}");
}
}
}
#[cfg(test)]
mod estate_signup_tests {
//! ★ **A signup makes its account on the estate it CAME FROM** (ONE box, two sites of
//! gunnar, owner 2026-10-11): the caller's own bearer ([`crate::auth::CallerEstate`]) for
//! an account at the shop, the account its money went through for a paid one — never the
//! product's home estate by default.
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
use monetize_pay::signup::{NameVerdict, PaidSignup, SignupError, SignupOutcome, SignupSink};
use monetize_proto::signup_server::Signup as _;
use super::*;
const TEST2: &str = "test2-gunnar-rs";
/// Says on which estate each account was asked for and each name checked.
#[derive(Default)]
struct Sited {
paid: Mutex<Vec<(String, Option<String>)>>,
named: Mutex<Vec<(String, Option<String>)>>,
queue_first: Mutex<bool>,
}
impl SignupSink for Sited {
fn paid(&self, s: &PaidSignup) -> Result<SignupOutcome, SignupError> {
self.paid_on(s, None)
}
fn paid_on(
&self,
s: &PaidSignup,
estate: Option<&str>,
) -> Result<SignupOutcome, SignupError> {
self.paid
.lock()
.unwrap()
.push((s.username.clone(), estate.map(str::to_owned)));
if std::mem::take(&mut *self.queue_first.lock().unwrap()) {
return Err(SignupError::Unavailable(format!(
"{}the estate's main makes it",
crate::signup_pull::QUEUED
)));
}
Ok(SignupOutcome::Created {
namespace: s.username.clone(),
username: s.username.clone(),
})
}
fn name_verdict(&self, username: &str) -> Result<NameVerdict, SignupError> {
self.name_verdict_on("", username, None)
}
fn name_verdict_on(
&self,
_: &str,
username: &str,
estate: Option<&str>,
) -> Result<NameVerdict, SignupError> {
self.named
.lock()
.unwrap()
.push((username.to_owned(), estate.map(str::to_owned)));
Ok(NameVerdict::Free)
}
}
fn state(reg: crate::registry::Registry, sink: Arc<Sited>) -> Arc<AppState> {
let mut s = AppState::in_memory(reg);
s.signup_sink = sink;
let mut plans = crate::signup::tests::plans();
plans.insert(
"gunnar/account".into(),
crate::config::SignupPlan {
amount_minor: 0,
currency: monetize_pay::Currency::Sek,
cap_bytes: 0,
period_days: 0,
},
);
s.signup_plans = plans.into();
let tick = Arc::new(AtomicU64::new(1_788_436_800_000));
s.clock = Box::new(move || tick.load(std::sync::atomic::Ordering::SeqCst));
Arc::new(s)
}
fn from_test2<T>(body: T) -> Request<T> {
let mut req = Request::new(body);
req.extensions_mut()
.insert(crate::auth::CallerEstate(TEST2.into()));
req
}
/// An account at the shop asked for from test2's console is made on test2 — at once,
/// and again when it was queued and the poll settles it later (the estate is kept with
/// the record); its name is checked on test2.
#[tokio::test]
async fn an_account_from_an_estates_console_is_made_on_that_estate() {
let sink = Arc::new(Sited::default());
*sink.queue_first.lock().unwrap() = true;
let s = state(crate::registry::Registry::new(), sink.clone());
let req = pb::StartSignupRequest {
username: "nina".into(),
product: "gunnar".into(),
plan: "gunnar/account".into(),
email: "nina@example.com".into(),
success_url: "https://test2.gunnar.rs/welcome?session={session}".into(),
..Default::default()
};
let out = SignupService(s.clone())
.start(from_test2(req))
.await
.expect("a queued account is started")
.into_inner();
assert_eq!(
sink.named.lock().unwrap().clone(),
vec![("nina".to_string(), Some(TEST2.to_string()))],
"the name is checked on test2"
);
assert_eq!(
sink.paid.lock().unwrap().clone(),
vec![("nina".to_string(), Some(TEST2.to_string()))],
"asked on test2"
);
let rec = s
.signups
.get(crate::signup::ACCOUNT_VENDOR, &out.session)
.unwrap()
.expect("kept pending");
assert_eq!(
rec.estate.as_deref(),
Some(TEST2),
"the record keeps where it began"
);
crate::signup::settle(&s, &rec.signup);
assert_eq!(
sink.paid.lock().unwrap().last().cloned(),
Some(("nina".to_string(), Some(TEST2.to_string()))),
"the later settle asks test2 again, never the home"
);
let checked = SignupService(s.clone())
.check_name(from_test2(pb::CheckNameRequest {
username: "olle".into(),
product: "gunnar".into(),
}))
.await
.unwrap()
.into_inner();
assert!(checked.free);
assert_eq!(
sink.named.lock().unwrap().last().cloned(),
Some(("olle".to_string(), Some(TEST2.to_string()))),
"CheckName asks test2"
);
}
/// ★ ST-11: **a site's signup asks ITS OWN estate whether the room is there** (STINKY
/// 2026-10-11): a priced plan started from test2's console asks the capacity of test2's
/// main (the brand-new tenant `@test2-gunnar-rs/<name>` routes there), never gunnar.rs's;
/// the home site still asks the brand-new-tenant question with no name.
#[tokio::test]
async fn a_sites_signup_asks_the_room_of_that_sites_own_estate() {
let product = Arc::new(monetize::testing::MockProduct::gunnar());
product.absorbs_on_hand();
let site = |vendor: &str, site: &str| crate::config::EstateConfig {
product: "gunnar".into(),
vendor: vendor.into(),
site: site.into(),
..Default::default()
};
let reg = crate::registry::Registry::new()
.sells("gunnar", monetize::testing::MockPay::new())
.shared_product(product.clone())
.home("gunnar", "gunnar-rs")
.estate("gunnar-rs", site("mockpay", "gunnar.rs"))
.estate(TEST2, site("mockpay", "test2.gunnar.rs"));
let s = state(reg, Arc::new(Sited::default()));
let ask = |name: &str, host: &str| pb::StartSignupRequest {
username: name.into(),
product: "gunnar".into(),
plan: crate::signup::tests::PLAN.into(),
email: format!("{name}@example.invalid"),
success_url: format!("https://{host}/welcome?session={{session}}"),
cancel_url: format!("https://{host}/signup"),
..Default::default()
};
let _ = SignupService(s.clone())
.start(from_test2(ask("nina", "test2.gunnar.rs")))
.await;
assert_eq!(
product.absorb_tenants().last().map(String::as_str),
Some("@test2-gunnar-rs/nina"),
"test2's own main is asked: {:?}",
product.absorb_tenants()
);
let mut home = Request::new(ask("olle", "gunnar.rs"));
home.extensions_mut()
.insert(crate::auth::CallerEstate("gunnar-rs".into()));
let _ = SignupService(s.clone()).start(home).await;
assert_eq!(
product.absorb_tenants().last().map(String::as_str),
Some(""),
"the home asks the brand-new-tenant question: {:?}",
product.absorb_tenants()
);
}
/// ★ ST-6: **one address, one account PER SITE** (owner 2026-10-11): an address with an
/// account on gunnar.rs signs up on test2.gunnar.rs (no account exists there), and is
/// refused a second account on the site where it already has one — never told "sign in"
/// on a site it has no account on.
#[tokio::test]
async fn an_address_with_an_account_on_one_site_signs_up_on_the_other() {
let sink = Arc::new(Sited::default());
let s = state(crate::registry::Registry::new(), sink.clone());
let ask = |name: &str| pb::StartSignupRequest {
username: name.into(),
product: "gunnar".into(),
plan: "gunnar/account".into(),
email: "Nina@Example.invalid".into(),
success_url: "https://test2.gunnar.rs/welcome?session={session}".into(),
..Default::default()
};
SignupService(s.clone())
.start(Request::new(ask("nina")))
.await
.expect("an account on the home site");
SignupService(s.clone())
.start(from_test2(ask("nina2")))
.await
.expect("the same address on test2: no account there yet");
let again = SignupService(s.clone())
.start(from_test2(ask("nina3")))
.await
.expect_err("test2 already has its account");
assert_eq!(again.message(), crate::signup::words::EMAIL_HAS_ACCOUNT);
let home = SignupService(s.clone())
.start(Request::new(ask("nina4")))
.await
.expect_err("the home already has its account");
assert_eq!(home.message(), crate::signup::words::EMAIL_HAS_ACCOUNT);
}
/// A paid signup's webhook names no caller: the account its money went through
/// (`[estates.<estate>] vendor`) names the estate it is made on.
#[test]
fn a_paid_signup_is_made_on_the_estate_whose_account_took_the_money() {
let sink = Arc::new(Sited::default());
let reg = crate::registry::Registry::new().estate(
TEST2,
crate::config::EstateConfig {
product: "gunnar".into(),
vendor: "mockpay".into(),
..Default::default()
},
);
let s = state(reg, sink.clone());
crate::signup::settle(&s, &crate::signup::tests::a_signup("cs_t2", "bob"));
assert_eq!(
sink.paid.lock().unwrap().clone(),
vec![("bob".to_string(), Some(TEST2.to_string()))]
);
// A vendor no estate bills through: the product's home, as before.
let home = Arc::new(Sited::default());
let s = state(crate::registry::Registry::new(), home.clone());
crate::signup::settle(&s, &crate::signup::tests::a_signup("cs_home", "bob"));
assert_eq!(
home.paid.lock().unwrap().clone(),
vec![("bob".to_string(), None)]
);
}
}
#[cfg(test)]
mod card_check_tests {
//! ★ **A free account only after a card check, one per card per site** (owner
//! 2026-10-15): the vendor checks a card with nothing charged; the account is made when
//! the welcome page asks `Signup.Status` and the vendor says the card is checked; a card
//! that already has a free account on the site is refused in plain words; paid plans are
//! untouched.
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
use monetize_pay::signup::{NameVerdict, PaidSignup, SignupError, SignupOutcome, SignupSink};
use monetize_proto::signup_server::Signup as _;
use super::*;
use crate::testing::CallbackPay;
const HOME: &str = "gunnar-rs";
const TEST2: &str = "test2-gunnar-rs";
/// Makes every account it is asked for, and says which.
#[derive(Default)]
struct Made(Mutex<Vec<(String, Option<String>)>>);
impl SignupSink for Made {
fn paid(&self, s: &PaidSignup) -> Result<SignupOutcome, SignupError> {
self.paid_on(s, None)
}
fn paid_on(
&self,
s: &PaidSignup,
estate: Option<&str>,
) -> Result<SignupOutcome, SignupError> {
self.0
.lock()
.unwrap()
.push((s.username.clone(), estate.map(str::to_owned)));
Ok(SignupOutcome::Created {
namespace: s.username.clone(),
username: s.username.clone(),
})
}
fn name_verdict(&self, _: &str) -> Result<NameVerdict, SignupError> {
Ok(NameVerdict::Free)
}
}
struct World {
s: Arc<AppState>,
pay: CallbackPay,
sink: Arc<Made>,
}
fn world() -> World {
let pay = CallbackPay::with_checkout("whsec", "https://pay.example/c/");
let site = |site: &str| crate::config::EstateConfig {
product: "gunnar".into(),
vendor: "mockpay".into(),
site: site.into(),
..Default::default()
};
let reg = crate::registry::Registry::new()
.sells("gunnar", pay.clone())
.home("gunnar", HOME)
.estate(HOME, site("gunnar.rs"))
.estate(TEST2, site("test2.gunnar.rs"));
let sink = Arc::new(Made::default());
let mut s = AppState::in_memory(reg);
s.signup_sink = sink.clone();
let mut plans = crate::signup::tests::plans();
plans.insert(
"gunnar/account".into(),
crate::config::SignupPlan {
amount_minor: 0,
currency: monetize_pay::Currency::Sek,
cap_bytes: 0,
period_days: 0,
},
);
s.signup_plans = plans.into();
let tick = Arc::new(AtomicU64::new(1_788_436_800_000));
s.clock = Box::new(move || tick.load(std::sync::atomic::Ordering::SeqCst));
World {
s: Arc::new(s),
pay,
sink,
}
}
fn on<T>(estate: &str, body: T) -> Request<T> {
let mut req = Request::new(body);
req.extensions_mut()
.insert(crate::auth::CallerEstate(estate.into()));
req
}
fn host(estate: &str) -> &'static str {
if estate == TEST2 {
"test2.gunnar.rs"
} else {
"gunnar.rs"
}
}
fn free(name: &str, estate: &str) -> pb::StartSignupRequest {
pb::StartSignupRequest {
username: name.into(),
product: "gunnar".into(),
plan: "gunnar/account".into(),
vendor: "mockpay".into(),
email: format!("{name}@example.com"),
success_url: format!("https://{}/welcome?session={{session}}", host(estate)),
cancel_url: format!("https://{}/signup", host(estate)),
..Default::default()
}
}
impl World {
async fn start(&self, name: &str, estate: &str) -> Result<pb::StartSignupResponse, Status> {
SignupService(self.s.clone())
.start(on(estate, free(name, estate)))
.await
.map(Response::into_inner)
}
async fn status(&self, session: &str, estate: &str) -> pb::SignupStatusResponse {
SignupService(self.s.clone())
.status(on(
estate,
pb::SignupStatusRequest {
vendor: "mockpay".into(),
session: session.into(),
},
))
.await
.expect("status answers")
.into_inner()
}
/// Start a free account, check `card` at the vendor, and read the welcome page's poll.
async fn sign_up_with(
&self,
name: &str,
estate: &str,
card: &str,
) -> pb::SignupStatusResponse {
let page = self
.start(name, estate)
.await
.expect("a card check is minted");
self.pay.check_card(&page.session, card);
self.status(&page.session, estate).await
}
fn made(&self) -> Vec<String> {
self.sink
.0
.lock()
.unwrap()
.iter()
.map(|(n, _)| n.clone())
.collect()
}
}
use pb::signup_status_response::State;
/// The free account is NOT made at Start: the buyer is sent to the vendor's card check
/// (nothing charged), and the account is made once the vendor says the card is checked —
/// with the card's fingerprint on its record, never more of the card.
#[tokio::test]
async fn a_free_account_is_made_only_after_the_card_check() {
let w = world();
The file goes on; the console shows all of it.