crates/monetize-server/src/estate_order/price.rs
//! **What a private estate costs, from the provider's own answer** (owner,
//! 2026-10-04 18:00).
//!
//! * The buyer sizes FRONT and MAIN from the provider's plan list
//! ([`monetize_cloud::Cloud::plan_catalog`]); main's data disk in GiB.
//! The TWIN is never sized by the buyer: its plan follows main's
//! ([`monetize_cloud::lay::twin_plan_for`]: an eighth of it, never under
//! one core and 4 GB) and its data disk holds main's whole data set.
//! * The provider prices the whole estate ([`monetize_cloud::Cloud::price_estate`]).
//! * **Customer price = provider cost + 20 %** (`margin_bp`), per month.
//! * **Six months, prepaid** (`term_months`): the hold is six months' price,
//! captured when the estate answers.
//! * **At the buy** — the moment the job is about to lay — the provider's
//! price is read AGAIN. If the price the buyer was quoted would leave less
//! than 10 % (`min_margin_bp`) over the provider's cost, nothing is bought:
//! the order is refused with a sentence and the hold is released.
//!
//! # The currency: the provider's own (OWNER DECISION, 2026-10-04 19:45)
//!
//! A private gunnar is priced in EUR, the currency UpCloud bills in, so the
//! margin is computed between two numbers in one currency and there is no
//! exchange rate anywhere in this code. Stripe Checkout shows the buyer SEK
//! or DKK (Adaptive Pricing) and does the conversion; its cost sits inside the
//! margin. An offer configured in another currency than the provider's is
//! refused, never converted. monetize's other products stay in SEK/DKK.
use monetize_cloud::lay::{twin_plan_floored, PlanSpec, TWIN_FLOOR_MEMORY_MIB};
use monetize_cloud::{CloudError, Cloud, Price, Zone};
use monetize_pay::{Currency, Money};
use super::EstateOrderConfig;
/// **The buyer's choice**: front's and main's plans, and main's data disk per
/// member. Empty fields are the shape's defaults.
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EstateSizes {
#[serde(default)]
pub front_plan: String,
#[serde(default)]
pub main_plan: String,
/// GiB per data member (main has four); 0 = the shape's.
#[serde(default)]
pub main_data_gib: u64,
/// **The buyer's own order for a parametric product** (monetize v2.0 P1 in process, P3 on
/// the wire: proto `EstateSizes.order`): the values the product's order form names, checked
/// and filled in by [`order_of`] before anything is priced or held; what its guest's plan
/// reads (`LayPlanReq.sizes.order`). Null for a product sized by the three fields above
/// (Gunnar, Holger), and then never written, so their runs and plans read as before.
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub order: serde_json::Value,
}
/// Pure: **the order checked against the product's form** (v2.0 P3, the parametric order):
/// `sizes` with its order filled in from the form's defaults ([`crate::facts::OrderForm::check`]),
/// or the refusal that names the first value that is wrong. A product with no form of its own is
/// sized by the three plan fields, and an order for it is refused by name.
pub fn order_of(facts: &crate::facts::Facts, sizes: &EstateSizes) -> Result<EstateSizes, String> {
let order = match (&facts.form, &sizes.order) {
(Some(form), given) => form.check(given)?,
(None, serde_json::Value::Null) => serde_json::Value::Null,
(None, _) => return Err(format!("this product ({}) takes no order of its own: an order names its plans (front, main, data) and nothing else", if facts.kind.is_empty() { "unnamed" } else { &facts.kind })),
};
Ok(EstateSizes { order, ..sizes.clone() })
}
/// **The order as the wire carries it** (proto `EstateSizes.order`): a JSON object, or empty
/// for none; `Err` names what is not one.
pub fn order_from_wire(text: &str) -> Result<serde_json::Value, String> {
let text = text.trim();
if text.is_empty() {
return Ok(serde_json::Value::Null);
}
match serde_json::from_str::<serde_json::Value>(text) {
Ok(v @ serde_json::Value::Object(_)) => Ok(v),
Ok(other) => Err(format!("an order is a set of named values (a JSON object), not {other}")),
Err(e) => Err(format!("the order is not JSON: {e}")),
}
}
/// The order as the wire carries it: empty for none.
pub fn order_to_wire(order: &serde_json::Value) -> String {
if order.is_null() { String::new() } else { order.to_string() }
}
/// One machine as the buyer is shown it: the provider's own numbers.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MachineLine {
pub role: String,
pub plan: String,
pub cores: u32,
pub memory_mib: u64,
/// Every disk of the machine, GiB (the system disk first).
pub disks_gib: Vec<u64>,
}
/// **A priced estate**: what the provider charges, what the buyer pays.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Quote {
/// The sizes, resolved: every plan named, the twin's chosen.
pub sizes: EstateSizes,
pub twin_plan: String,
pub machines: Vec<MachineLine>,
/// The provider's cost a month, in the provider's currency.
pub cost: Price,
/// The cost a month in the sale's currency.
pub cost_in_sale: Money,
/// The buyer's price a month: cost + `margin_bp`.
pub monthly: Money,
pub margin_bp: u32,
pub term_months: u32,
/// A term of days ([`super::EstateOrderConfig::term_days`]); 0: `term_months`.
#[serde(default)]
pub term_days: u32,
/// What is held now and captured when the estate answers: `monthly × term`.
pub total: Money,
/// The plans the buyer may choose for front and main (the provider's own,
/// within `[estate_orders] plans`), smallest first.
#[serde(default)]
pub offered: Vec<PlanSpec>,
}
impl Quote {
/// The price line the buy page shows, as words (monetize renders money).
pub fn price_words(&self) -> String {
format!(
"provider {} + {} % = {} a month; {} prepaid: {}",
self.cost_in_sale.in_words(),
self.margin_bp / 100,
self.monthly.in_words(),
term_words(self.term_months, self.term_days),
self.total.in_words()
)
}
}
/// Pure: **the term, as words** — `1 day`, `3 days`, `1 month`, `6 months`.
pub fn term_words(months: u32, days: u32) -> String {
match (days, months) {
(1, _) => "1 day".into(),
(d, _) if d > 1 => format!("{d} days"),
(_, 1) => "1 month".into(),
(_, m) => format!("{m} months"),
}
}
/// Pure: **the term as a fraction of a month**, `(numerator, denominator)`: `(months, 1)`, or
/// `(days, 30)` for a term of days (the monthly price, pro rata).
pub fn term_fraction(months: u32, days: u32) -> (u64, u64) {
if days > 0 {
(u64::from(days), 30)
} else {
(u64::from(months.max(1)), 1)
}
}
/// Pure: **the term's one charge** of a monthly price: `monthly × months`, or `monthly × days /
/// 30` rounded up.
pub fn term_charge(monthly_minor: u64, months: u32, days: u32) -> u64 {
let (n, d) = term_fraction(months, days);
mul_div_ceil(monthly_minor, n, d)
}
/// Pure: **the end of a term** starting at `from`: `days` whole days, or `months` calendar months.
pub fn add_term(from: u64, months: u32, days: u32) -> u64 {
if days > 0 {
from.saturating_add(u64::from(days) * 86_400_000)
} else {
add_months(from, months)
}
}
/// `ceil(a × b / c)`, in u128 so a large price cannot overflow.
fn mul_div_ceil(a: u64, b: u64, c: u64) -> u64 {
((a as u128 * b as u128).div_ceil(c as u128)) as u64
}
/// **The currency a sale is in**, and the rate from the provider's.
pub fn sale_currency(cfg: &EstateOrderConfig) -> Result<Currency, String> {
match cfg.currency.trim().to_ascii_uppercase().as_str() {
"" => Err("[estate_orders] currency is not set (SEK, EUR or DKK)".into()),
c => Currency::from_code(c).ok_or_else(|| format!("[estate_orders] currency {c:?} is not SEK, EUR or DKK")),
}
}
/// **The provider's cost in the sale's currency** — which must BE the
/// provider's: no rate, no conversion (owner, 2026-10-04 19:45).
pub fn convert(cfg: &EstateOrderConfig, cost: Price) -> Result<Money, String> {
let to = sale_currency(cfg)?;
if cost.currency != to {
return Err(format!(
"the provider prices in {} and this offer is in {}: a private {} is sold in the provider's currency, with no conversion",
cost.currency.code(),
to.code(),
super::lifecycle::name_of(cfg)
));
}
Ok(Money { minor: cost.minor_per_month, currency: to })
}
/// **The payment vendor's fee on one charge** of `charge_minor`: the fixed
/// part plus the percentage, rounded up — monetize's own rule for room
/// (`monetize::pricing::PriceRule::net_after_fee_minor`), configured in
/// `[estate_orders] fee_fixed_minor / fee_bp`.
pub fn fee_minor(cfg: &EstateOrderConfig, charge_minor: u64) -> u64 {
let rule = monetize::pricing::PriceRule { fee_fixed_minor: cfg.fee_fixed_minor, fee_bp: cfg.fee_bp, ..monetize::pricing::PriceRule::default() };
charge_minor.saturating_sub(rule.net_after_fee_minor(charge_minor))
}
/// **THE FLOOR, net of the payment fee** (2026-10-05): the term's charge
/// (`monthly × term_months`, held and captured as ONE charge) less the
/// vendor's fee must cover the provider's cost for the term plus
/// `min_margin_bp`. The fee is cost, so a margin that is 10 % before it is
/// not 10 % after it.
pub fn floor_holds(cfg: &EstateOrderConfig, monthly_minor: u64, live_cost_minor: u64) -> bool {
let (n, d) = term_fraction(cfg.term_months, cfg.term_days);
let charge = term_charge(monthly_minor, cfg.term_months, cfg.term_days);
let net = u128::from(charge - fee_minor(cfg, charge).min(charge));
// net / (cost × n / d) ≥ 1 + min: in integers, net × d × 10 000 ≥ cost × n × (10 000 + min).
net * u128::from(d) * 10_000 >= u128::from(live_cost_minor) * u128::from(n) * (10_000 + u128::from(cfg.min_margin_bp))
}
/// ★ **The smallest monthly price at or over `monthly_minor` that holds the floor** for a cost
/// of `cost_minor` a month — the payment fee priced in, never a refusal (owner rule: every size
/// is buyable, T2H-NUMBERS 2026-10-25). On a short term the fee's fixed part can eat the whole
/// margin of a cheap step (test2.holger.rs's one-day Mini: 8.4 % left at + 25 %); the step is
/// then sold at the price that leaves `min_margin_bp` after the fee. `None` only where no price
/// can (a fee of 100 % or more).
pub fn floor_price_minor(cfg: &EstateOrderConfig, monthly_minor: u64, cost_minor: u64) -> Option<u64> {
if floor_holds(cfg, monthly_minor, cost_minor) {
return Some(monthly_minor);
}
if cfg.fee_bp >= 10_000 {
return None;
}
let (n, d) = term_fraction(cfg.term_months, cfg.term_days);
// The net the term's one charge must leave, then the charge that leaves it, then the
// monthly price whose charge is that: an upper bound (the fee's rounding is stepped past).
let need = (u128::from(cost_minor) * u128::from(n) * (10_000 + u128::from(cfg.min_margin_bp))).div_ceil(u128::from(d) * 10_000);
let charge = ((need + u128::from(cfg.fee_fixed_minor)) * 10_000).div_ceil(10_000 - u128::from(cfg.fee_bp));
let mut hi = u64::try_from((charge * u128::from(d)).div_ceil(u128::from(n))).ok()?.max(monthly_minor);
let mut steps = 0;
while !floor_holds(cfg, hi, cost_minor) {
hi = hi.checked_add(1 + hi / 1_000)?;
steps += 1;
if steps > 64 {
return None;
}
}
// The floor only rises with the price: the smallest price that holds it, by halves.
let mut lo = monthly_minor;
while lo + 1 < hi {
let mid = lo + (hi - lo) / 2;
if floor_holds(cfg, mid, cost_minor) {
hi = mid;
} else {
lo = mid;
}
}
Some(hi)
}
/// **The margin over cost that is left after the fee**, basis points, over
/// the term's one charge.
pub fn net_margin_bp_of(cfg: &EstateOrderConfig, monthly_minor: u64, cost_minor: u64) -> u64 {
let charge = term_charge(monthly_minor, cfg.term_months, cfg.term_days);
let net = charge.saturating_sub(fee_minor(cfg, charge));
let cost = term_charge(cost_minor, cfg.term_months, cfg.term_days);
if cost == 0 {
return 0;
}
(net.saturating_sub(cost) as u128 * 10_000 / cost as u128) as u64
}
/// **Does the price the buyer was quoted still leave `min_margin_bp` over
/// what the provider charges now?** `monthly ≥ cost × (1 + min)`, exactly.
pub fn margin_holds(monthly_minor: u64, live_cost_minor: u64, min_margin_bp: u32) -> bool {
(monthly_minor as u128) * 10_000 >= (live_cost_minor as u128) * (10_000 + min_margin_bp as u128)
}
/// The margin a monthly price leaves over a cost, in basis points (rounded down).
pub fn margin_bp_of(monthly_minor: u64, cost_minor: u64) -> u64 {
if cost_minor == 0 {
return u64::MAX;
}
((monthly_minor.saturating_sub(cost_minor)) as u128 * 10_000 / cost_minor as u128) as u64
}
/// The buyer's choice, resolved against the provider's list: every plan named
/// and sold, main's data size in bounds, the twin chosen. Refused by name.
pub fn resolve(cfg: &EstateOrderConfig, catalog: &[PlanSpec], asked: &EstateSizes) -> Result<(EstateSizes, PlanSpec, PlanSpec, PlanSpec), String> {
let pick = |asked: &str, default: &str, role: &str| -> Result<PlanSpec, String> {
let name = if asked.trim().is_empty() { default } else { asked.trim() };
if !cfg.plans.is_empty() && !cfg.plans.iter().any(|p| p == name) {
return Err(format!("the plan {name:?} is not offered for {role} (offered: {})", cfg.plans.join(", ")));
}
catalog.iter().find(|p| p.name == name).cloned().ok_or_else(|| format!("the provider does not sell the plan {name:?} for {role}"))
};
let front = pick(&asked.front_plan, &cfg.shape.front.plan, "front")?;
let main = pick(&asked.main_plan, &cfg.shape.main.plan, "main")?;
let data = if asked.main_data_gib == 0 { cfg.shape.main.disks.get(1).map(|d| d.gib).unwrap_or(10) } else { asked.main_data_gib };
if !(10..=4096).contains(&data) || data % 10 != 0 {
return Err(format!("main's data disks are 10 to 4096 GB each, in steps of 10; {data} is not"));
}
let twin = twin_plan_floored(catalog, &main, cfg.shape.twin_floor_mib.unwrap_or(TWIN_FLOOR_MEMORY_MIB)).cloned().ok_or_else(|| format!("the provider sells no plan big enough for the twin of a {} main", main.name))?;
Ok((EstateSizes { front_plan: front.name.clone(), main_plan: main.name.clone(), main_data_gib: data, order: asked.order.clone() }, front, main, twin))
}
/// **Price an estate**: the provider's plan list, the provider's price for
/// the whole estate, the rate, the margin, the term. PURE at the provider
/// (reads only). What the buy page shows and what `Start` holds.
pub fn quote(cfg: &EstateOrderConfig, cloud: &dyn Cloud, company: &str, asked: &EstateSizes) -> Result<Quote, String> {
quote_with_twin(cfg, cloud, company, asked, "")
}
/// [`quote`], keeping a twin that GREW by itself (`twin_floor`, the
/// autoscale's plan): it stands while it is bigger than the rule's choice and
/// no bigger than main; otherwise the rule decides (an eighth of main).
pub fn quote_with_twin(cfg: &EstateOrderConfig, cloud: &dyn Cloud, company: &str, asked: &EstateSizes, twin_floor: &str) -> Result<Quote, String> {
quote_as_laid(cfg, cloud, company, asked, twin_floor, "")
}
/// [`quote_with_twin`] for an estate that STANDS (its renewal, its reminder): `laid_twin`,
/// the plan its twin is laid at, is the twin that is priced — whatever the rule would give
/// a new order of these sizes. The rule changes (2026-10-13: the twin's floor went from 2
/// to 4 GB), and an estate laid under the old one is neither resized nor charged for the
/// new one by that; only a plan change its buyer makes ([`super::change::change`], which
/// asks [`quote_with_twin`]) moves its twin to the rule's. Empty, or a plan the provider no
/// longer lists: the rule decides, as for a new order.
pub fn quote_as_laid(cfg: &EstateOrderConfig, cloud: &dyn Cloud, company: &str, asked: &EstateSizes, twin_floor: &str, laid_twin: &str) -> Result<Quote, String> {
if cfg.margin_bp < cfg.min_margin_bp {
return Err(format!(
"the configured margin ({} %) is under the floor ({} %): nothing is sold at it",
cfg.margin_bp as f64 / 100.0,
cfg.min_margin_bp as f64 / 100.0
));
}
if cfg.term_months == 0 && cfg.term_days == 0 {
return Err("[estate_orders] term_months is 0".into());
}
let zone = Zone(cfg.zone.clone());
let catalog = monetize_cloud::ask::plan_catalog(&cloud, &zone).map_err(|e| format!("the provider's plan list: {e}"))?;
let (sizes, _front, main, mut twin) = resolve(cfg, &catalog, asked)?;
if let Some(floor) = catalog.iter().find(|p| !twin_floor.is_empty() && p.name == twin_floor) {
let bigger = (floor.cores, floor.memory_mib) > (twin.cores, twin.memory_mib) && floor.cores >= twin.cores && floor.memory_mib >= twin.memory_mib;
if bigger && floor.cores <= main.cores && floor.memory_mib <= main.memory_mib {
twin = floor.clone();
}
}
if let Some(laid) = catalog.iter().find(|p| !laid_twin.is_empty() && p.name == laid_twin) {
twin = laid.clone();
}
let plan = super::lifecycle::lay_plan_of(cfg, company, &sizes, &twin.name)?;
let cost = monetize_cloud::Cloud::price(&cloud, &monetize_cloud::cloud::Priced::Plan(&plan)).map_err(|e| format!("the provider's price: {e}"))?;
let cost_in_sale = convert(cfg, cost)?;
let at_margin = Money { minor: mul_div_ceil(cost_in_sale.minor, 10_000 + cfg.margin_bp as u64, 10_000), currency: cost_in_sale.currency };
// The floor, net of the payment fee, already at the quote: a margin that is configured
// over 10 % may leave less after the fee (a cheap step on a short term), and then the fee
// is priced in — the smallest price that keeps the floor ([`floor_price_minor`]); every
// size stays buyable. Only where no price can keep it is nothing sold.
let lifted = floor_price_minor(cfg, at_margin.minor, cost_in_sale.minor);
let Some(lifted) = lifted else {
let monthly = at_margin;
return Err(format!(
"at provider + {:.1} %, what is left after the payment fee ({:.2} + {:.1} % of the {} charge) is {:.1} %, under the {} % we sell at: nothing is sold at this price",
cfg.margin_bp as f64 / 100.0,
cfg.fee_fixed_minor as f64 / 100.0,
cfg.fee_bp as f64 / 100.0,
term_words(cfg.term_months, cfg.term_days),
net_margin_bp_of(cfg, monthly.minor, cost_in_sale.minor) as f64 / 100.0,
cfg.min_margin_bp / 100
));
};
let monthly = Money { minor: lifted, currency: at_margin.currency };
let margin_bp = if lifted == at_margin.minor { cfg.margin_bp } else { u32::try_from(margin_bp_of(lifted, cost_in_sale.minor)).unwrap_or(u32::MAX) };
let total = Money { minor: term_charge(monthly.minor, cfg.term_months, cfg.term_days), currency: monthly.currency };
let spec = |name: &str| catalog.iter().find(|p| p.name == name).cloned();
let machines = plan
.machines
.iter()
.map(|m| {
let s = spec(&m.plan);
MachineLine { role: m.role.clone(), plan: m.plan.clone(), cores: s.as_ref().map(|s| s.cores).unwrap_or(0), memory_mib: s.map(|s| s.memory_mib).unwrap_or(0), disks_gib: m.disks.iter().map(|d| d.gib).collect() }
})
.collect();
let mut offered: Vec<PlanSpec> = catalog.iter().filter(|p| cfg.plans.is_empty() || cfg.plans.contains(&p.name)).cloned().collect();
offered.sort_by_key(|p| (p.cores, p.memory_mib, p.name.clone()));
// ★ A33-5: a price the terms HOLD (the operator's own buy) is the term's and the month's,
// the machines and their cost named as for anyone.
let (monthly, total) = match cfg.held_price_minor {
0 => (monthly, total),
held => (Money { minor: held, currency: monthly.currency }, Money { minor: held, currency: monthly.currency }),
};
Ok(Quote { sizes, twin_plan: twin.name, machines, cost, cost_in_sale, monthly, margin_bp, term_months: cfg.term_months, term_days: cfg.term_days, total, offered })
}
/// **The buy-time check**: the provider's price read again for exactly what
/// is about to be laid. `Err` is the sentence the buyer is told.
pub fn recheck(cfg: &EstateOrderConfig, cloud: &dyn Cloud, plan: &monetize_cloud::lay::EstatePlan, monthly: Money) -> Result<Money, String> {
let live_in_sale = live_price(cfg, cloud, plan)?;
// A held price (the operator's own buy, [`super::priced_for`]) is not held to the floor.
if cfg.held_price_minor > 0 || floor_holds(cfg, monthly.minor, live_in_sale.minor) {
Ok(live_in_sale)
} else {
Err(format!(
"The provider's price for your machines rose to {} a month since you ordered. At the {} a month you were quoted, our margin after the payment fee would be {:.1} %, under the {} % we sell at, so nothing was bought and nothing is charged: the hold is released.",
live_in_sale.in_words(),
monthly.in_words(),
net_margin_bp_of(cfg, monthly.minor, live_in_sale.minor) as f64 / 100.0,
cfg.min_margin_bp / 100
))
}
}
/// **The provider's price NOW** for exactly `plan`, in the sale's currency — no floor asked
/// (the operator's own buy, owner 2026-10-12; [`recheck`] asks it of everyone else).
/// `Err` starting `unavailable:` is a provider that did not answer.
pub fn live_price(cfg: &EstateOrderConfig, cloud: &dyn Cloud, plan: &monetize_cloud::lay::EstatePlan) -> Result<Money, String> {
let live = monetize_cloud::Cloud::price(&cloud, &monetize_cloud::cloud::Priced::Plan(plan)).map_err(|e| match e {
CloudError::Unavailable(w) => format!("unavailable: {w}"),
other => format!("the provider's price: {other}"),
})?;
convert(cfg, live)
}
/// **`months` calendar months after `unix_ms`** (UTC; the day clamped to the
/// month's last), for a term's `paid_until`.
pub fn add_months(unix_ms: u64, months: u32) -> u64 {
let days = (unix_ms / 86_400_000) as i64;
let rem = unix_ms % 86_400_000;
// Civil from days (H. Hinnant).
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
let total = y * 12 + (m - 1) + months as i64;
let (ny, nm) = (total.div_euclid(12), total.rem_euclid(12) + 1);
let leap = (ny % 4 == 0 && ny % 100 != 0) || ny % 400 == 0;
let last = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][(nm - 1) as usize];
let nd = d.min(last);
monetize_pay::time::ymd_to_unix_ms(ny, nm as u32, nd as u32).unwrap_or(unix_ms) + rem
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn twenty_percent_holds_and_nine_point_nine_does_not() {
// The buyer pays 1200 a month; the provider charged 1000 at the quote.
assert!(margin_holds(1_200, 1_000, 1_000), "20 % over cost: bought");
// The provider's price rose: 1200 / 1.099 = 1091.9 → 9.9 % left.
assert!(!margin_holds(1_200, 1_092, 1_000), "9.9 % over cost: refused");
assert!(margin_holds(1_100, 1_000, 1_000), "exactly 10 %: still bought");
assert!(!margin_holds(1_099, 1_000, 1_000), "9.9 %: refused");
assert_eq!(margin_bp_of(1_200, 1_000), 2_000);
assert_eq!(margin_bp_of(1_099, 1_000), 990);
}
#[test]
fn the_cost_is_taken_in_the_providers_currency_and_never_converted() {
let eur = Price { minor_per_month: 10_000, currency: Currency::Eur };
let cfg = EstateOrderConfig { currency: "EUR".into(), ..EstateOrderConfig::default() };
assert_eq!(convert(&cfg, eur).unwrap(), Money { minor: 10_000, currency: Currency::Eur });
let dkk = EstateOrderConfig { currency: "DKK".into(), ..EstateOrderConfig::default() };
assert!(convert(&dkk, eur).unwrap_err().contains("with no conversion"));
}
/// ★ **The 10 % floor is what is left AFTER the payment fee** (2026-10-05,
/// the coordinator's measurement: at × 1.20 over a cost that left out the
/// IPv4 addresses the real margin was 5.9 %). The fee is monetize's room
/// rule (Stripe 0.25 + 1.5 % of the charge, the six months in one charge).
#[test]
fn the_floor_is_net_of_the_payment_fee() {
let cfg = EstateOrderConfig::default();
assert_eq!((cfg.fee_fixed_minor, cfg.fee_bp), (25, 150), "the room's Stripe rule");
// Cost 90.00 a month (the default shape with its three IPv4), sold at
// 108.00: 648.00 for six months, fee 0.25 + 9.72, net 638.03, over
// 90.00 × 6 × 1.10 = 594.00 — holds, at 18.1 % net.
assert!(floor_holds(&cfg, 10_800, 9_000));
assert_eq!(net_margin_bp_of(&cfg, 10_800, 9_000), 1_815);
// 11 % before the fee is not 10 % after it: 99.90 against 90.00.
assert!(margin_holds(9_990, 9_000, 1_000), "before the fee: 11 %");
assert!(!floor_holds(&cfg, 9_990, 9_000), "after the fee: 9.3 %, under the floor");
assert!(net_margin_bp_of(&cfg, 9_990, 9_000) < 1_000);
}
#[test]
fn six_months_after_is_a_calendar_date() {
let at = monetize_pay::time::ymd_to_unix_ms(2026, 10, 4).unwrap() + 3_600_000;
assert_eq!(add_months(at, 6), monetize_pay::time::ymd_to_unix_ms(2027, 4, 4).unwrap() + 3_600_000);
let aug31 = monetize_pay::time::ymd_to_unix_ms(2026, 8, 31).unwrap();
assert_eq!(add_months(aug31, 6), monetize_pay::time::ymd_to_unix_ms(2027, 2, 28).unwrap(), "clamped to February's last day");
}
}