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

nordisk/monetize

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"
    );
}