crates/monetize-server/src/plans/tests.rs
//! The server half of the price rule: the basis read through the cloud seam
//! and cached, and N counted from what was paid last month.
use std::sync::atomic::Ordering;
use std::sync::Arc;
use monetize::testing::{MockCloud, MockPay, MockProduct};
use monetize_embed::civil::unix_ms_from_civil;
use monetize_pay::Money;
use super::*;
use crate::config::{Config, MachineConfig};
use crate::registry::Registry;
const GIB: u64 = 1 << 30;
/// The owner's estate on the mock: three servers at 10.00 EUR each through
/// the cloud and their public IPv4 addresses at 3.51 EUR each from the same
/// list (+ 96.80 EUR stated, what the seam cannot price) = 137.33 EUR a month; a GiB of main's data tier at 0.10 EUR through the cloud
/// + 0.1599 EUR for the twin's replica tier = 0.2599 EUR.
pub(crate) fn owners_pricing() -> PricingConfig {
PricingConfig {
currency: Currency::Eur,
base_extra_month_minor: 9_680,
replica_gib_month_milli: 15_990,
fx: Fx(
[("SEK".to_string(), 110_000u64), ("DKK".to_string(), 74_600)]
.into_iter()
.collect(),
),
plans: [
("10gb-1m", 10, Period::Month),
("10gb-12m", 10, Period::Year),
("100gb-12m", 100, Period::Year),
("500gb-12m", 500, Period::Year),
]
.into_iter()
.map(|(id, gib, period)| {
(
id.to_string(),
PlanSpec {
size_bytes: gib * GIB,
period,
target: None,
},
)
})
.collect(),
..PricingConfig::default()
}
}
/// `[machines]` for the owner's estate, every box on the mock cloud.
pub(crate) fn owners_machines() -> Config {
let m = |role: &str, plan: &str| MachineConfig {
role: role.into(),
hostname: format!("gunnar-{role}"),
cloud: "mockcloud".into(),
server: format!("srv-{role}"),
plan: plan.into(),
..Default::default()
};
Config {
machines: [
("main".to_string(), m("appliance", "4xCPU-8GB")),
("twin".to_string(), m("twin", "2xCPU-4GB")),
("front".to_string(), m("front", "1xCPU-2GB")),
]
.into(),
..Config::default()
}
}
fn state(cloud: Arc<MockCloud>) -> AppState {
let mut s = AppState::in_memory(
Registry::new()
.shared_product(Arc::new(MockProduct::gunnar()))
.cloud(cloud),
);
*s.config.write().unwrap() = owners_machines();
s.plans = PlanBook::new([("gunnar".to_string(), owners_pricing())].into());
s
}
/// ★ **The basis is the PROVIDER's price**, read through the cloud seam: the
/// servers of `[machines]` and a GiB of main's data tier, plus what the seam
/// cannot price, stated — the owner's 137.33 EUR and 0.26 EUR per GiB.
#[test]
fn the_basis_is_read_from_the_providers_price_and_is_the_owners_estate() {
let cloud = Arc::new(MockCloud::new());
let s = state(cloud.clone());
let b = read_basis(&s, "gunnar").expect("priced");
assert_eq!(
b,
CostBasis {
currency: Currency::Eur,
base_month_minor: 13_733,
storage_gib_month_milli: 25_990
}
);
assert_eq!(cloud.prices_asked.load(Ordering::SeqCst), 4 + 9, "three server plans and one data disk, and the nine plans of the zone's catalog, read once for its address price");
assert_eq!(
cloud.ipv4_asked.load(Ordering::SeqCst),
1,
"and the public IPv4 address, once for the zone every machine is in"
);
let r = PriceRule::default();
assert_eq!(
Money {
minor: r.monthly_minor(&b, 10 * GIB, 20),
currency: Currency::Eur
}
.in_words(),
"13.71 EUR"
);
// A provider that prices in another currency than the estate is paid in
// is not converted at a guess.
let mut wrong = owners_pricing();
wrong.currency = Currency::Sek;
s.plans
.products
.write()
.unwrap()
.insert("gunnar".into(), wrong);
let e = read_basis(&s, "gunnar").unwrap_err();
assert!(e.contains("currency"), "{e}");
}
/// ★ **THE PUBLIC IPv4 ADDRESSES ARE THE PROVIDER'S PRICE** (owner,
/// 2026-10-05: the base counted 3 × a typed 3.00 EUR; UpCloud's list says
/// `ipv4_address` = 3.51): each machine's address from its cloud's price
/// list, the same book that prices the servers and the disk — a list at 4.00
/// is 3 × 4.00 in the base. The stated fallback (3.51 unless said) only when
/// the provider cannot price an address.
#[test]
fn the_base_counts_each_machines_public_ipv4_at_the_providers_price_and_the_fallback_only_without_one(
) {
let base_without_ipv4 = 3 * 1_000 + 9_680;
let cloud = Arc::new(MockCloud::new());
*cloud.ipv4_minor.lock().unwrap() = Some(400);
let s = state(cloud.clone());
assert_eq!(
read_basis(&s, "gunnar").unwrap().base_month_minor,
base_without_ipv4 + 3 * 400,
"three addresses at the list's 4.00"
);
// A provider that cannot price an address: the stated fallback, 3.51 by default.
let cloud = Arc::new(MockCloud::new());
*cloud.fail_ipv4.lock().unwrap() = "no ipv4_address in this list".into();
let s = state(cloud.clone());
assert_eq!(owners_pricing().ipv4_fallback_month_minor, 351);
assert_eq!(
read_basis(&s, "gunnar").unwrap().base_month_minor,
base_without_ipv4 + 3 * 351,
"the fallback, 3.51 a month each"
);
let mut said = owners_pricing();
said.ipv4_fallback_month_minor = 300;
s.plans
.products
.write()
.unwrap()
.insert("gunnar".into(), said);
assert_eq!(
read_basis(&s, "gunnar").unwrap().base_month_minor,
base_without_ipv4 + 3 * 300,
"a stated fallback"
);
}
/// ★ **CACHED at least 10 minutes**: a second reading inside the life asks
/// the provider nothing; past it, one reading.
#[test]
fn the_basis_is_cached_for_ten_minutes_and_read_again_after() {
let cloud = Arc::new(MockCloud::new());
let s = state(cloud.clone());
assert!(s.plans.cached("gunnar").is_none(), "nothing read yet");
read_basis(&s, "gunnar").unwrap();
let asked = cloud.prices_asked.load(Ordering::SeqCst);
s.plans.age_for_test(BASIS_FRESH - Duration::from_secs(5));
assert!(
s.plans.cached("gunnar").is_some(),
"still fresh at 9 min 55 s"
);
assert_eq!(
cloud.prices_asked.load(Ordering::SeqCst),
asked,
"a cached basis asks the provider nothing"
);
s.plans.age_for_test(Duration::from_secs(10));
assert!(
s.plans.cached("gunnar").is_none(),
"stale past 10 minutes: the next asker reads again"
);
}
/// A cloud that cannot price says so, by name; a stated basis needs no cloud.
#[test]
fn a_cloud_that_cannot_price_is_named_and_a_stated_basis_needs_none() {
let cloud = Arc::new(MockCloud::new());
cloud.fail_price("the price book is down");
let s = state(cloud);
let e = read_basis(&s, "gunnar").unwrap_err();
assert!(
e.contains("could not price") && e.contains("price book is down"),
"{e}"
);
let mut stated = owners_pricing();
stated.basis = Some(StatedBasis {
base_month_minor: 13_580,
storage_gib_month_milli: 25_990,
});
s.plans
.products
.write()
.unwrap()
.insert("gunnar".into(), stated);
assert_eq!(read_basis(&s, "gunnar").unwrap().base_month_minor, 13_580);
}
/// ★ **N = last month's paying accounts, never under 20** — distinct
/// tenants who paid in the PREVIOUS calendar month (a granted sale at a
/// price, or a paid signup), counted once a month.
#[test]
fn n_is_last_months_distinct_paying_accounts_and_never_under_twenty() {
let cloud = Arc::new(MockCloud::new());
let mut s = state(cloud);
let september = unix_ms_from_civil(2026, 9, 10);
let october = unix_ms_from_civil(2026, 10, 5);
let p = MockProduct::gunnar();
// 25 tenants paid in September, one of them twice; three paid in October.
for i in 0..26u32 {
let t = if i == 25 {
"t0".to_string()
} else {
format!("t{i}")
};
let mut buy = monetize::testing::purchase(&t);
buy.starts_unix_ms = september + u64::from(i) * 60_000;
s.clock = Box::new(move || september);
s.transaction(p.served(), monetize_pay::Vendor::of(&MockPay::new()))
.run(&buy)
.expect("paid");
}
s.clock = Box::new(move || october);
for i in 0..3u32 {
let mut buy = monetize::testing::purchase(&format!("new{i}"));
buy.starts_unix_ms = october;
s.transaction(p.served(), monetize_pay::Vendor::of(&MockPay::new()))
.run(&buy)
.expect("paid");
}
assert_eq!(
paying_last_month(&s, "gunnar", october),
25,
"distinct tenants, September only"
);
let rule = PriceRule::default();
s.clock = Box::new(move || october);
assert_eq!(accounts(&s, "gunnar", &rule), 25);
assert_eq!(
rule.accounts(paying_last_month(
&s,
"gunnar",
unix_ms_from_civil(2026, 9, 20)
)),
20,
"August had none: N is 20"
);
}
/// **test2's estate** (2026-10-05): the basis that read 100.54 SEK for
/// 10 GiB · 1 month and 966.24 SEK for 10 GiB · 12 months by the rule (base
/// 73.00 EUR a month with its three public IPv4 addresses at a typed 3.00,
/// storage 0.2599 EUR per GiB-month), the addresses now at the provider's
/// 3.51 — 74.53 EUR — the SEK and DKK rates of the kit, and the owner's two
/// targets: 10 GiB · 1 month 110 SEK, 10 GiB · 12 months 890 SEK.
fn test2_pricing() -> (PricingConfig, CostBasis) {
let mut cfg = owners_pricing();
cfg.plans.get_mut("10gb-1m").unwrap().target = Some([("SEK".to_string(), 11_000u64)].into());
cfg.plans.get_mut("10gb-12m").unwrap().target = Some([("SEK".to_string(), 89_000u64)].into());
(
cfg,
CostBasis {
currency: Currency::Eur,
base_month_minor: 7_300 + 3 * (351 - 300),
storage_gib_month_milli: 25_990,
},
)
}
fn priced(
cfg: &PricingConfig,
basis: &CostBasis,
n: u32,
to: Currency,
id: &str,
current: Option<&Current>,
now: u64,
) -> Priced {
let row = plan::rows(&cfg.rule, current, &cfg.plans, now)
.into_iter()
.find(|r| r.id == id)
.unwrap_or_else(|| panic!("no row {id}"));
let p = price_row(cfg, basis, n, to, &row, current, now).expect("priced");
assert!(
clears_floor(cfg, &p),
"{id} at N={n} in {to:?}: {} is under the floor",
p.price.in_words()
);
p
}
/// ★ **The owner's two prices are TARGETS on two plans** (2026-10-05:
/// "10 GiB monthly 110+", "12 months 890"; "do the professional choice"),
/// on test2. 10 GiB · 1 month is max(110, rule, floor): 110.00 SEK at
/// N = 20 and N = 30 (the rule says 101.75 and 82.28). 10 GiB · 12 months is
/// max(floor, 890) (owner 2026-10-06: FIXED at the target): the floor at
/// N = 20 (935.33) and 21 (909.15), the target from 22 on (890.00), also at
/// N = 30 where the rule says 790.68 — never under the floor.
/// Every plan without a target — 100 and 500 GiB — is the rule's, which did
/// not move.
#[test]
fn the_owners_prices_are_targets_raising_the_month_and_lowering_the_year_and_nothing_else_moves() {
let (cfg, b) = test2_pricing();
let now = unix_ms_from_civil(2026, 10, 5);
let at =
|n: u32, to: Currency, id: &str| priced(&cfg, &b, n, to, id, None, now).price.in_words();
assert_eq!(
(
at(20, Currency::Sek, "10gb-1m"),
at(30, Currency::Sek, "10gb-1m")
),
("110.00 SEK".into(), "110.00 SEK".into()),
"the month target raises it"
);
assert_eq!(
at(20, Currency::Sek, "10gb-12m"),
"935.33 SEK",
"the floor wins at N = 20 (the rule says 976.80)"
);
assert_eq!(
at(21, Currency::Sek, "10gb-12m"),
"909.15 SEK",
"still the floor (the rule says 950.40)"
);
assert_eq!(
at(22, Currency::Sek, "10gb-12m"),
"890.00 SEK",
"the target (the rule says 926.64)"
);
assert_eq!(
at(30, Currency::Sek, "10gb-12m"),
"890.00 SEK",
"FIXED at the target (owner 2026-10-06): the rule's 790.68 does not lower it"
);
// The month a target year locks: its twelfth, up; the rule's own month when the rule stands.
assert_eq!(
priced(&cfg, &b, 22, Currency::Sek, "10gb-12m", None, now)
.month_price
.in_words(),
"74.17 SEK"
);
assert_eq!(
priced(&cfg, &b, 30, Currency::Sek, "10gb-12m", None, now)
.month_price
.in_words(),
"74.17 SEK",
"the year stays at the target, so its month too"
);
// Another market: the SEK target back in EUR (rounded down) and on.
assert_eq!(at(22, Currency::Eur, "10gb-12m"), "80.90 EUR");
assert_eq!(at(22, Currency::Dkk, "10gb-12m"), "603.52 DKK");
assert_eq!(at(20, Currency::Eur, "10gb-1m"), "10.00 EUR");
// No target: the rule's price, the rule being today's (× 1.40, × 0.80).
assert_eq!((cfg.rule.markup_bp, cfg.rule.yearly_bp), (14_000, 8_000));
for (id, gib, n, want) in [
("100gb-12m", 100, 20, "4488.00 SEK"),
("100gb-12m", 100, 30, "4301.88 SEK"),
("500gb-12m", 500, 20, "20091.72 SEK"),
("500gb-12m", 500, 30, "19905.60 SEK"),
] {
let rule = cfg
.fx
.convert(
Money {
minor: cfg.rule.yearly_total_minor(&b, gib * GIB, n),
currency: Currency::Eur,
},
Currency::Sek,
)
.unwrap();
assert_eq!(
(at(n, Currency::Sek, id), rule.in_words()),
(want.to_string(), want.to_string()),
"{id} at N = {n}"
);
}
// An UPGRADE from a month paid at the target to 10 GiB · 12 months is
// the target year less the unused month.
let month = Current {
period: Period::Month,
size_bytes: 10 * GIB,
paid_until_unix_ms: now + 15 * monetize_embed::civil::DAY_MS,
state: monetize_product::State::Paid,
month_price: Some(Money {
minor: 11_000,
currency: Currency::Sek,
}),
};
let up = priced(&cfg, &b, 22, Currency::Sek, "10gb-12m", Some(&month), now);
let credit = up.credit.expect("the unused month is credited");
assert_eq!(
up.price.minor + credit.minor,
89_000,
"the target year, less the credit: {} + {}",
up.price.in_words(),
credit.in_words()
);
// A PROLONG of that month keeps its locked 110.00 SEK.
let prolong = priced(
&cfg,
&b,
30,
Currency::Sek,
plan::PROLONG_ID,
Some(&month),
now,
);
assert_eq!(prolong.price.in_words(), "110.00 SEK");
}
/// ★ **OWNER DECISION 2026-10-06: the 10 GiB · 12 months price is FIXED at
/// 890 SEK — it does not fall with scale** (it was max(floor, min(rule, 890)):
/// 872.52 SEK at N = 25 and 803.88 at N = 30 on the live list). Now max(floor,
/// 890): never under cost + 10 % after the fee (the owner's standing rule),
/// never under the target. Basis: UpCloud's LIVE se-sto1 list, read 2026-10-06
/// (GET /1.3/price): STARTER-4xCPU-8GB 2.9761 c/h → 21.73 EUR, -4xCPU-16GB
/// 4.1666 → 30.42, -1xCPU-2GB 0.8928 → 6.52, ipv4_address 0.4812 → 3.51 each,
/// storage_maxiops 0.031 → 0.2263 EUR/GiB-month; the kit's twin replica 0.0599.
/// Base 69.20 EUR, storage 0.2862 EUR/GiB-month, fx 11.00.
#[test]
fn the_yearly_ten_gib_price_is_fixed_at_its_target_and_never_falls_with_scale() {
let (mut cfg, _) = test2_pricing();
cfg.replica_gib_month_milli = 5_990;
let live = CostBasis {
currency: Currency::Eur,
base_month_minor: 2_173 + 3_042 + 652 + 3 * 351,
storage_gib_month_milli: 22_630 + 5_990,
};
let now = unix_ms_from_civil(2026, 10, 6);
let at = |n: u32, id: &str| {
priced(&cfg, &live, n, Currency::Sek, id, None, now)
.price
.in_words()
};
assert_eq!(
at(20, "10gb-12m"),
"934.89 SEK",
"the cost floor still wins at N = 20 (the target is under cost + 10 %)"
);
assert_eq!(
at(25, "10gb-12m"),
"890.00 SEK",
"FIXED at the target — the rule's 872.52 does not lower it"
);
assert_eq!(
at(30, "10gb-12m"),
"890.00 SEK",
"FIXED at the target — the rule's 803.88 does not lower it"
);
assert_eq!(at(200, "10gb-12m"), "890.00 SEK", "at any scale");
assert_eq!(
(at(20, "10gb-1m"), at(30, "10gb-1m")),
("110.00 SEK".into(), "110.00 SEK".into()),
"the month is untouched"
);
}
/// ★ **OWNER 2026-10-06 "make it simple": 890 SEK from day one.** The kit
/// states `[pricing.gunnar.rule] min_accounts = 22`, so N = max(paying, 22):
/// with ONE paying account (a new estate, test2) the live list prices
/// 10 GiB · 12 months at 890.00 SEK and 10 GiB · 1 month at 110.00 SEK.
#[test]
fn with_twenty_two_accounts_as_the_least_the_yearly_ten_gib_is_890_from_day_one() {
let (mut cfg, _) = test2_pricing();
cfg.replica_gib_month_milli = 5_990;
cfg.rule.min_accounts = 22;
let live = CostBasis {
currency: Currency::Eur,
base_month_minor: 2_173 + 3_042 + 652 + 3 * 351,
storage_gib_month_milli: 22_630 + 5_990,
};
let now = unix_ms_from_civil(2026, 10, 6);
let n = cfg.rule.accounts(1);
assert_eq!(n, 22, "one paying account: N is the least the kit states");
assert_eq!(
priced(&cfg, &live, n, Currency::Sek, "10gb-12m", None, now)
.price
.in_words(),
"890.00 SEK"
);
assert_eq!(
priced(&cfg, &live, n, Currency::Sek, "10gb-1m", None, now)
.price
.in_words(),
"110.00 SEK"
);
}
/// ★ **Sign-up by the room rule** (round 5.3.1, owner 2026-10-08: ONE bar at
/// sign-up too): a priced id names its plan and the price the checkout charged,
/// and reads back to the declared size and period at that price; a plan that is
/// not declared, a 1-month plan past 10 GiB, or any other id is no priced plan.
#[test]
fn a_priced_sign_up_id_reads_back_to_its_declared_plan_at_its_price() {
let price = Money {
minor: 89_000,
currency: Currency::Sek,
};
let id = priced_id("gunnar", "10gb-12m", &price);
assert_eq!(id, "gunnar/10gb-12m@89000SEK");
assert_eq!(
parse_priced_id(&id),
Some(("gunnar".to_owned(), "10gb-12m".to_owned(), price))
);
assert_eq!(
parse_priced_id("gunnar/10gb-1m"),
None,
"a table plan is no priced plan"
);
assert_eq!(
parse_priced_id("gunnar/10gb-1m@12XYZ"),
None,
"an unknown currency"
);
assert_eq!(parse_priced_id("@100SEK"), None);
let s = state(Arc::new(MockCloud::new()));
let year = priced_signup_plan(&s, &id).expect("a declared 12-month plan");
assert_eq!(
(
year.cap_bytes,
year.period_days,
year.amount_minor,
year.currency
),
(10 * GIB, 365, 89_000, Currency::Sek)
);
let big = priced_signup_plan(&s, "gunnar/500gb-12m@512345EUR").expect("500 GiB for 12 months");
assert_eq!((big.cap_bytes, big.period_days), (500 * GIB, 365));
let month = priced_signup_plan(&s, "gunnar/10gb-1m@11000SEK").expect("the 1-month plan");
assert_eq!((month.cap_bytes, month.period_days), (10 * GIB, 30));
assert!(
priced_signup_plan(&s, "gunnar/1000gb-12m@1SEK").is_none(),
"a plan this deployment does not declare is never sold"
);
assert!(
priced_signup_plan(&s, "holger/10gb-12m@1SEK").is_none(),
"a product with no [pricing]"
);
let mut wide = owners_pricing();
wide.plans.insert(
"100gb-1m".into(),
PlanSpec {
size_bytes: 100 * GIB,
period: Period::Month,
target: None,
},
);
s.plans
.products
.write()
.unwrap()
.insert("gunnar".into(), wide);
assert!(
priced_signup_plan(&s, "gunnar/100gb-1m@1SEK").is_none(),
"a 1-month plan is ONLY 10 GiB (owner 2026-10-05)"
);
}
/// ★ **A new account's plans are the rule's, priced now in its market** — the
/// 1-month 10 GiB first, then the 12-month ones by size — and the START checks
/// a priced id against today's price: the shown price is current, a stale or a
/// forged one is not.
#[test]
fn a_new_accounts_plans_are_the_rules_and_a_start_checks_the_price_shown() {
let cloud = Arc::new(MockCloud::new());
let s = Arc::new(state(cloud));
let (cfg, basis) = test2_pricing();
s.plans
.products
.write()
.unwrap()
.insert("gunnar".into(), cfg);
s.plans.store("gunnar", basis);
let rooms = signup_rooms_cached(&s, "gunnar", Market::Se).expect("priced");
let names: Vec<&str> = rooms.iter().map(|r| r.name.as_str()).collect();
assert_eq!(rooms.len(), 4, "{names:?}");
assert_eq!(rooms[0].plan.period_days, 30, "the month first: {names:?}");
assert!(
rooms[1..].iter().all(|r| r.plan.period_days == 365),
"then the years: {names:?}"
);
assert!(
rooms
.windows(2)
.skip(1)
.all(|w| w[0].plan.cap_bytes < w[1].plan.cap_bytes),
"by size: {names:?}"
);
assert!(
rooms
.iter()
.all(|r| r.plan.currency == Currency::Sek && r.id.ends_with("SEK")),
"one market's currency"
);
assert_eq!(
rooms[0].plan.amount_minor, 11_000,
"the owner's 110 SEK month"
);
for r in &rooms {
assert_eq!(
priced_id_is_current(&s, &r.id, Market::Se),
Ok(true),
"{} is today's price",
r.id
);
assert_eq!(
priced_signup_plan(&s, &r.id),
Some(r.plan),
"the paid check reads the same terms"
);
}
let forged = priced_id(
"gunnar",
"500gb-12m",
&Money {
minor: 100,
currency: Currency::Sek,
},
);
assert_eq!(
priced_id_is_current(&s, &forged, Market::Se),
Ok(false),
"a price that is not today's is never started"
);
assert!(
priced_id_is_current(&s, "gunnar/1000gb-12m@1SEK", Market::Se).is_err(),
"a plan not sold here"
);
}
/// The kit's `[pricing.gunnar.plans]` (private-gunnar-ops `MONETIZE_COMMERCIAL`,
/// owner test2 2026-10-08: the room bar 10 GB → 1 TB): the month at 10 GiB, then
/// 12 months at 10 / 50 / 100 / 250 / 500 / 1000 GiB, the owner's two targets.
fn the_kits_pricing() -> PricingConfig {
let mut cfg = owners_pricing();
cfg.rule.min_accounts = 22;
cfg.plans = [
("10gb-1m", 10, Period::Month),
("10gb-12m", 10, Period::Year),
("50gb-12m", 50, Period::Year),
("100gb-12m", 100, Period::Year),
("250gb-12m", 250, Period::Year),
("500gb-12m", 500, Period::Year),
("1000gb-12m", 1000, Period::Year),
]
.into_iter()
.map(|(id, gib, period)| {
(
id.to_string(),
PlanSpec {
size_bytes: gib * GIB,
period,
target: None,
},
)
})
.collect();
cfg.plans.get_mut("10gb-1m").unwrap().target = Some([("SEK".to_string(), 11_000u64)].into());
cfg.plans.get_mut("10gb-12m").unwrap().target = Some([("SEK".to_string(), 89_000u64)].into());
cfg
}
/// ★ **A NEW visitor's bar is the same priced stops an account grows on**
/// (owner on test2 2026-10-09: "+ / − don't work" — the sign-up offered one
/// step). Over the wire, `Signup.Plans` answers the kit's seven stops: 10 GB ·
/// 1 month first, then the 12-month stops up to 1 TB, in the visitor's market
/// currency, each the rule's price and never under the floor; `Signup.Start`
/// on the stop chosen mints the checkout at exactly that price, and the plan
/// it settles to is exactly that room for that period.
#[tokio::test]
async fn a_new_visitor_is_offered_every_stop_and_the_checkout_charges_the_one_chosen() {
use crate::services::signup::SignupService;
use monetize_proto::signup_server::Signup as _;
use monetize_proto::{self as pb};
let pay = crate::testing::CallbackPay::with_checkout(
"stripe-shared-secret",
"https://checkout.example/pay/",
);
let mut st = AppState::in_memory(
Registry::new()
.sells("gunnar", pay.clone())
.cloud(MockCloud::new()),
);
*st.config.write().unwrap() = owners_machines();
let cfg = the_kits_pricing();
st.plans = PlanBook::new([("gunnar".to_string(), cfg.clone())].into());
st.signup_sink = crate::signup::tests::CountingSink::creating();
let s = Arc::new(st);
let basis = read_basis(&s, "gunnar").expect("the provider's price");
s.plans.store("gunnar", basis);
let now = s.now();
let n = accounts(&s, "gunnar", &cfg.rule);
for (market, currency) in [
(pb::Market::Se, Currency::Sek),
(pb::Market::Dk, Currency::Dkk),
(pb::Market::Fi, Currency::Eur),
] {
let out = SignupService(s.clone())
.plans(tonic::Request::new(pb::SignupPlansRequest {
product: "gunnar".into(),
market: market as i32,
}))
.await
.expect("plans")
.into_inner();
let names: Vec<&str> = out.plans.iter().map(|p| p.name.as_str()).collect();
assert_eq!(
out.plans.len(),
7,
"{market:?}: the month and six 12-month stops: {names:?}"
);
assert_eq!(out.market, market as i32);
assert_eq!(
(out.plans[0].cap_bytes, out.plans[0].period_days),
(10 * GIB, 30),
"10 GB · 1 month first: {names:?}"
);
let years: Vec<u64> = out.plans[1..].iter().map(|p| p.cap_bytes / GIB).collect();
assert_eq!(years, [10, 50, 100, 250, 500, 1000], "{names:?}");
assert!(
out.plans[1..].iter().all(|p| p.period_days == 365),
"{names:?}"
);
for p in &out.plans {
let (_, row_id, price) =
parse_priced_id(&p.id).unwrap_or_else(|| panic!("{} is a priced id", p.id));
assert_eq!(
price.currency, currency,
"{market:?}: one market's currency: {}",
p.id
);
// The rule's price, the floor held (`priced` asserts `clears_floor`).
let rule = priced(&cfg, &basis, n, currency, &row_id, None, now);
assert_eq!(price, rule.price, "{}", p.id);
let words =
format!("{} {} {}", p.name, p.resources_words, p.price_words).to_ascii_lowercase();
assert!(
!words.contains("left") && !words.contains("cap"),
"a visitor never reads a cap: {words}"
);
}
let prices: std::collections::BTreeSet<u64> = out
.plans
.iter()
.map(|p| parse_priced_id(&p.id).unwrap().2.minor)
.collect();
assert_eq!(prices.len(), 7, "the price moves with every stop");
}
// The visitor chose the top stop (and then one between): the checkout is
// minted at exactly that price, and the plan is exactly that room and period.
let se = SignupService(s.clone())
.plans(tonic::Request::new(pb::SignupPlansRequest {
product: "gunnar".into(),
market: pb::Market::Se as i32,
}))
.await
.unwrap()
.into_inner();
for (i, chosen) in [se.plans[6].clone(), se.plans[2].clone()]
.into_iter()
.enumerate()
{
let user = format!("olle{i}");
let started = SignupService(s.clone())
.start(tonic::Request::new(pb::StartSignupRequest {
product: "gunnar".into(),
username: user.clone(),
email: format!("{user}@example.com"),
plan: chosen.id.clone(),
market: pb::Market::Se as i32,
success_url: "https://test2.gunnar.rs/welcome?session={session}".into(),
cancel_url: "https://test2.gunnar.rs/signup".into(),
..Default::default()
}))
.await
.unwrap_or_else(|e| panic!("{}: {e}", chosen.id))
.into_inner();
assert!(
started.url.starts_with("https://checkout.example/pay/"),
"{started:?}"
);
let order = pay
.signup_orders()
.last()
.cloned()
.expect("an order reached the vendor");
let (_, _, price) = parse_priced_id(&chosen.id).unwrap();
assert_eq!(
(order.plan.as_str(), order.amount),
(chosen.id.as_str(), price),
"the checkout charges the stop shown"
);
let plan = priced_signup_plan(&s, &chosen.id).expect("the plan it settles to");
assert_eq!(
(
plan.cap_bytes,
plan.period_days,
plan.amount_minor,
plan.currency
),
(chosen.cap_bytes, 365, price.minor, Currency::Sek),
"{}",
chosen.id
);
}
}