crates/monetize-server/src/estate_order/tests/pay_once.rs
//! ★ **A private Gunnar is paid at the vendor ONCE** (owner 2026-10-22: "ja! så måste det
//! vara!"; `DESIGN-PRIVATE-PAY-ONCE.md`). A visitor with no account types an e-mail and a name
//! and goes straight to the private payment: `EstateOrder.Start{new_account}` holds the name,
//! makes NOTHING before the money, and the account is made when the payment is PAID — the way
//! a paid signup's is (the sink, one record under `account`, the sign-in link mailed). The
//! estate order is that account's. The free account's card check is not on this path at all.
use monetize_proto as pb;
use monetize_proto::estate_order_server::EstateOrder as _;
use super::*;
use crate::services::estate_order::EstateOrderService;
const WELCOME: &str =
"https://gunnar.rs/welcome?session={session}&next=%2Fsignup%3Fplan%3Dprivate%26company%3Dacme";
/// A shop that makes accounts (the sink armed) and sells an account plan.
fn shop(name: &str) -> Rig {
let r = rig(name);
r.mails.3.store(true, Ordering::SeqCst);
let plans = std::collections::BTreeMap::from([(
"gunnar/account".to_string(),
crate::config::SignupPlan {
amount_minor: 0,
currency: monetize_pay::Currency::Sek,
cap_bytes: 0,
period_days: 0,
},
)]);
r.state.signup_plans.set(plans, Default::default());
r
}
/// A visitor's order: no ticket, a new account named `account` at `email`.
fn visitor(company: &str, account: &str, email: &str) -> pb::EstateOrderStartRequest {
pb::EstateOrderStartRequest {
company: company.into(),
email: email.into(),
account: account.into(),
new_account: true,
welcome_url: WELCOME.into(),
..Default::default()
}
}
fn rt() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
}
/// Drive `acme` until `done` — `Start`'s own nudge (a queued drive job) may hold the run's
/// drive for a moment, and a drive asked meanwhile returns at once.
fn driven(r: &Rig, done: impl Fn(&EstateRun) -> bool) {
until("the order's drive", || {
drive(&r.state, "acme");
r.state.estates.get("acme").is_some_and(|run| done(&run))
});
}
fn made(r: &Rig) -> Vec<String> {
r.mails.4.lock().unwrap().clone()
}
/// The account the order made, as the signup store keeps it.
fn account_records(r: &Rig) -> Vec<crate::signup::SignupRecord> {
r.state
.signups
.list(0)
.unwrap()
.into_iter()
.filter(|rec| rec.signup.vendor == crate::signup::ACCOUNT_VENDOR)
.collect()
}
/// **The whole path**: one vendor page (the estate's), nothing made before it is paid, the
/// account made at the payment — once — with its sign-in link mailed, and the order its own.
#[test]
fn a_visitor_pays_once_and_the_account_is_made_when_the_payment_is_paid() {
let r = shop("pay-once");
let svc = EstateOrderService(r.state.clone());
let rt = rt();
let started = rt
.block_on(svc.start(tonic::Request::new(visitor(
"acme",
"newbie",
"new@example.com",
))))
.expect("a visitor's order starts")
.into_inner();
assert!(
started.checkout_url.starts_with("https://pay.example/"),
"straight to the estate's payment: {started:?}"
);
assert_eq!(r.pay.0.requests.lock().unwrap().len(), 1, "ONE vendor page");
let run = r.state.estates.get("acme").unwrap();
assert_eq!(run.account, "newbie", "the order is the new account's");
let pending = run
.new_account
.clone()
.expect("the account to make is kept with the order");
assert!(
pending
.session
.starts_with(crate::signup::ACCOUNT_SESSION_PREFIX),
"{pending:?}"
);
assert!(
made(&r).is_empty() && account_records(&r).is_empty(),
"nothing is made before the money"
);
assert!(
r.state
.signups
.name_hold("gunnar", "", "newbie", r.state.now() as i64)
.unwrap()
.is_some(),
"the name is held while the page runs"
);
// Paid: the account is made, its link mailed; the estate goes on.
r.pay.authorize(&handle_of(&r, "acme"));
driven(&r, |run| run.phase == phase::CAPTURED);
let run = r.state.estates.get("acme").unwrap();
assert_eq!(run.phase, phase::CAPTURED, "{:#?}", run.history);
assert_eq!(
made(&r),
vec!["newbie".to_string()],
"the account is made at the payment"
);
let recs = account_records(&r);
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].signup.session, pending.session);
assert!(
matches!(&recs[0].state, crate::signup::SignupState::Created { username, .. } if username == "newbie"),
"{:?}",
recs[0].state
);
let link = r
.mails
.0
.lock()
.unwrap()
.iter()
.find(|n| n.kind == "account.sign_in")
.cloned()
.expect("the sign-in link is mailed");
assert_eq!(link.to, "new@example.com");
assert!(
link.body.contains(&format!(
"https://gunnar.rs/welcome?session={}&next=",
pending.session
)),
"{}",
link.body
);
assert!(
r.state
.signups
.name_hold("gunnar", "", "newbie", r.state.now() as i64)
.unwrap()
.is_none(),
"the hold is let go once the account is made"
);
// Once: another tick makes nothing more.
drive(&r.state, "acme");
assert_eq!(made(&r).len(), 1);
assert_eq!(r.mails.count("account.sign_in"), 1);
// The order's page says where the link went; the account's own ticket reads it whole.
let st = rt
.block_on(
svc.status(tonic::Request::new(pb::EstateOrderStatusRequest {
company: "acme".into(),
actor_ticket: Vec::new(),
})),
)
.unwrap()
.into_inner();
assert!(st.words.contains("sign-in link"), "{}", st.words);
assert!(
!st.words.contains("newbie"),
"a stranger's status names no account: {}",
st.words
);
let mine = rt
.block_on(
svc.status(tonic::Request::new(pb::EstateOrderStatusRequest {
company: "acme".into(),
actor_ticket: account_ticket("newbie", "n-mine"),
})),
)
.expect("the new account reads its order")
.into_inner();
assert_eq!(mine.url, "https://www.acme.gunnar.rs", "WWW-CANON");
// The address now has an account here: a second visitor order with it signs in instead.
let again = rt
.block_on(svc.start(tonic::Request::new(visitor(
"bolt",
"other",
"new@example.com",
))))
.unwrap_err();
assert_eq!(again.code(), tonic::Code::FailedPrecondition);
assert_eq!(again.message(), crate::signup::words::EMAIL_HAS_ACCOUNT);
}
/// **Refused by name before any money**: a taken name, a name held for somebody else's page,
/// a welcome page that is not https with `{session}`, a shop with no account plan, and a
/// visitor who neither proves an account nor asks for a new one.
#[test]
fn a_visitor_order_is_refused_before_any_money_moves() {
let r = shop("pay-once-refused");
r.mails.4.lock().unwrap().push("taken".into());
let svc = EstateOrderService(r.state.clone());
let rt = rt();
let ask = |req: pb::EstateOrderStartRequest| {
rt.block_on(svc.start(tonic::Request::new(req)))
.unwrap_err()
};
let e = ask(visitor("acme", "taken", "a@example.com"));
assert_eq!(e.code(), tonic::Code::FailedPrecondition, "{e:?}");
assert!(e.message().contains("taken"), "{e:?}");
let e = ask(visitor("acme", "Not A Name", "a@example.com"));
assert_eq!(e.code(), tonic::Code::FailedPrecondition, "{e:?}");
for url in [
"http://gunnar.rs/welcome?session={session}",
"https://gunnar.rs/welcome",
"",
] {
let e = ask(pb::EstateOrderStartRequest {
welcome_url: url.into(),
..visitor("acme", "newbie", "a@example.com")
});
assert_eq!(e.code(), tonic::Code::InvalidArgument, "{url}: {e:?}");
}
let e = ask(pb::EstateOrderStartRequest {
new_account: false,
..visitor("acme", "newbie", "a@example.com")
});
assert_eq!(
e.code(),
tonic::Code::Unauthenticated,
"no ticket and no new account: today's refusal"
);
// Somebody else's page holds the name.
rt.block_on(svc.start(tonic::Request::new(visitor(
"acme",
"newbie",
"a@example.com",
))))
.expect("the first visitor's order");
let e = ask(visitor("bolt", "newbie", "b@example.com"));
assert_eq!(e.code(), tonic::Code::FailedPrecondition);
assert_eq!(e.message(), crate::signup::words::NAME_BEING_BOUGHT);
assert_eq!(
r.pay.0.requests.lock().unwrap().len(),
1,
"only the first order reached the vendor"
);
// No account plan: no account can be made, so none is sold this way.
let r = shop("pay-once-no-plan");
r.state
.signup_plans
.set(Default::default(), Default::default());
let e = rt
.block_on(
EstateOrderService(r.state.clone()).start(tonic::Request::new(visitor(
"acme",
"newbie",
"a@example.com",
))),
)
.unwrap_err();
assert_eq!(e.code(), tonic::Code::FailedPrecondition, "{e:?}");
assert!(r.pay.0.requests.lock().unwrap().is_empty());
}
/// **A page never paid makes no account**: it expires, the name is free, nothing was made.
#[test]
fn an_unpaid_visitor_order_makes_no_account() {
let r = shop("pay-once-unpaid");
let svc = EstateOrderService(r.state.clone());
let rt = rt();
rt.block_on(svc.start(tonic::Request::new(visitor(
"acme",
"newbie",
"new@example.com",
))))
.expect("ordered");
r.clock.fetch_add(3_601 * 1_000, Ordering::SeqCst);
driven(&r, |run| run.phase == phase::RELEASED);
assert_eq!(r.state.estates.get("acme").unwrap().phase, phase::RELEASED);
assert!(
made(&r).is_empty() && account_records(&r).is_empty(),
"nothing was paid, nothing was made"
);
assert_eq!(r.mails.count("account.sign_in"), 0);
}
/// **A name taken after all, once paid** (the hold lapsed, an operator made it by hand): the
/// paid order is PARKED with an ALERT and nothing is laid for a stranger's account.
#[test]
fn a_paid_order_whose_name_was_taken_meanwhile_lays_nothing() {
let r = shop("pay-once-taken-late");
let svc = EstateOrderService(r.state.clone());
rt().block_on(svc.start(tonic::Request::new(visitor(
"acme",
"newbie",
"new@example.com",
))))
.expect("ordered");
r.mails.4.lock().unwrap().push("newbie".into());
r.pay.authorize(&handle_of(&r, "acme"));
driven(&r, |run| run.why.contains("ALERT"));
let run = r.state.estates.get("acme").unwrap();
assert_eq!(run.phase, phase::ORDERED, "{:#?}", run.history);
assert!(run.why.contains("ALERT"), "{}", run.why);
assert!(
r.cloud.lays().is_empty(),
"nothing is laid for an account that is not the buyer's"
);
}
/// ★ **The e-mail is not changed in the buy** (owner 2026-10-22: "man skall inte kunna byta
/// email"): a signed-in account's order goes to the address its account was made with — the one
/// monetize holds — whatever a page types; an account monetize holds no address for, with none
/// sent, is refused by name before any money.
#[test]
fn a_signed_in_accounts_order_uses_the_accounts_own_email() {
let r = shop("pay-once-own-email");
let now = r.state.now() as i64;
let signup = monetize_pay::signup::PaidSignup {
vendor: crate::signup::ACCOUNT_VENDOR.into(),
session: "acct_alice".into(),
reference: String::new(),
email: "alice@example.com".into(),
username: "alice".into(),
product: "gunnar".into(),
plan: "gunnar/account".into(),
amount: monetize_pay::Money {
minor: 0,
currency: monetize_pay::Currency::Sek,
},
tax: monetize_pay::Money {
minor: 0,
currency: monetize_pay::Currency::Sek,
},
paid_at_unix_ms: now,
customer_country: None,
};
r.state
.signups
.record_pending_on(&signup, now, None)
.unwrap();
r.state
.signups
.set(
crate::signup::ACCOUNT_VENDOR,
"acct_alice",
crate::signup::SignupState::Created {
namespace: "alice".into(),
username: "alice".into(),
at_unix_ms: now,
},
)
.unwrap();
let svc = EstateOrderService(r.state.clone());
let rt = rt();
let ask = |company: &str, account: &str, email: &str, nonce: &str| {
rt.block_on(svc.start(tonic::Request::new(pb::EstateOrderStartRequest {
company: company.into(),
email: email.into(),
account: account.into(),
actor_ticket: account_ticket(account, nonce),
..Default::default()
})))
};
ask("acme", "alice", "", "n-1").expect("no address typed: the account's own");
assert_eq!(
r.state.estates.get("acme").unwrap().email,
"alice@example.com"
);
ask("bolt", "alice", "typed@example.com", "n-2").expect("a typed address changes nothing");
assert_eq!(
r.state.estates.get("bolt").unwrap().email,
"alice@example.com"
);
let e = ask("colt", "bob", "", "n-3").unwrap_err();
assert_eq!(e.code(), tonic::Code::FailedPrecondition, "{e:?}");
assert!(r.state.estates.get("colt").is_none());
}
/// ★ **The button says what is paid** (owner 2026-10-22: "Pay <amount>"): every priced step of
/// the ladder carries the amount the vendor's page takes, as words alone — the amount its
/// `total_words` names.
#[test]
fn the_ladder_says_the_amount_each_step_is_paid_with() {
let r = shop("pay-once-ladder");
let l = rt()
.block_on(
EstateOrderService(r.state.clone())
.ladder(tonic::Request::new(pb::EstateOrderLadderRequest::default())),
)
.expect("the ladder")
.into_inner();
let priced: Vec<_> = l.steps.iter().filter(|s| s.available).collect();
assert!(!priced.is_empty());
for s in priced {
assert!(
!s.pay_words.is_empty() && s.total_words.ends_with(&s.pay_words),
"{:?} / {:?}",
s.pay_words,
s.total_words
);
assert!(!s.pay_words.contains("prepaid"), "{}", s.pay_words);
}
}
/// ★ **A10 row 6, for every console**: `EstateOrder.Start{cancel_url}` names the console's own
/// private choice; the vendor's page sends a buyer who backs out there. A cancel page on
/// another site is refused before anything is held.
#[test]
fn a_consoles_cancel_page_is_where_a_buyer_who_backs_out_returns() {
// The shop of [`shop`], its product's home on the site gunnar.rs (the pages are judged there).
let dir = tmp("pay-once-cancel");
let pay = HoldPay::default();
let cloud = Arc::new(MockCloud::new());
let kit = FakeKit::new("PASS");
let reg = Registry::new()
.sells("gunnar", pay.clone())
.cloud(cloud.clone())
.shared_product(shop_product())
.home("gunnar", "gunnar-rs")
.estate(
"gunnar-rs",
crate::config::EstateConfig {
product: "gunnar".into(),
vendor: "holdpay".into(),
site: "gunnar.rs".into(),
..Default::default()
},
);
let (state, clock, mails, health) = build_in(reg, &dir, cfg(), kit.clone(), None, None);
let r = Rig {
state,
pay,
cloud,
kit,
dir,
clock,
mails,
health,
};
r.mails.3.store(true, Ordering::SeqCst);
let plans = std::collections::BTreeMap::from([(
"gunnar/account".to_string(),
crate::config::SignupPlan {
amount_minor: 0,
currency: monetize_pay::Currency::Sek,
cap_bytes: 0,
period_days: 0,
},
)]);
r.state.signup_plans.set(plans, Default::default());
let svc = EstateOrderService(r.state.clone());
let rt = rt();
let off_site = pb::EstateOrderStartRequest {
cancel_url: "https://evil.example/signup?plan=private".into(),
..visitor("acme", "newbie", "new@example.com")
};
let on_site = |body: pb::EstateOrderStartRequest| {
let mut req = tonic::Request::new(body);
req.extensions_mut()
.insert(crate::auth::CallerEstate("gunnar-rs".into()));
req
};
let refused = rt
.block_on(svc.start(on_site(off_site)))
.expect_err("a cancel page on another site is refused");
assert_eq!(refused.code(), tonic::Code::InvalidArgument, "{refused:?}");
assert!(
r.pay.0.requests.lock().unwrap().is_empty(),
"nothing is held for it"
);
let asked = pb::EstateOrderStartRequest {
cancel_url: "https://gunnar.rs/signup?plan=private".into(),
..visitor("acme", "newbie", "new@example.com")
};
rt.block_on(svc.start(on_site(asked)))
.unwrap_or_else(|e| panic!("the order starts: {e:?}"));
assert_eq!(
r.pay.0.cancels.lock().unwrap().clone(),
vec![(
"estate/acme".to_string(),
"https://gunnar.rs/signup?plan=private".to_string()
)]
);
// ★ BUYFLOW-4: a buyer who paid comes back to the site they bought on — its own
// `/paid/estate/<co>`, the order's "paid, setting up" watch — whichever site the vendor's
// configured return page names (test3.holger.rs pays through test2's).
assert_eq!(
r.pay.0.back_tos.lock().unwrap().clone(),
vec![(
"estate/acme".to_string(),
"https://gunnar.rs/paid/estate/acme".to_string()
)],
"a paid buyer goes back to the site's own way back"
);
}