Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
const MAX_EXACT: i64 = 9007199254740991;
fn check_rate(name: &str, value: i64) {
if !(0..=10000).contains(&value) {
panic!("{} must be between 0 and 10000, received {}", name, value);
}
}
/// LTV, LTV:CAC and CAC payback, each computed as one exact division from the
/// inputs so no figure inherits another's rounding.
///
/// # Panics
/// Panics on negative amounts, rates outside 0..=10000, mixed currencies, or
/// inputs beyond the shared exact range.
pub fn ltv_cac(arpa: &Money, gross_margin_basis_points: i64, monthly_churn_basis_points: i64, cac: &Money) -> LtvCac {
assert_same_currency(arpa, cac);
if arpa.minor < 0 || cac.minor < 0 {
panic!(
"ARPA and CAC must not be negative, received {} and {}",
arpa.minor, cac.minor
);
}
check_rate("gross margin basis points", gross_margin_basis_points);
check_rate("monthly churn basis points", monthly_churn_basis_points);
// Monthly margin per account, in minor units x 10000. Checked in i128 so
// the guard itself cannot overflow.
let margin = arpa.minor * gross_margin_basis_points;
let churn = monthly_churn_basis_points;
if (margin as i128) * 10000 > MAX_EXACT as i128
|| (cac.minor as i128) * 10000 > MAX_EXACT as i128
|| (churn as i128) * (cac.minor as i128) > MAX_EXACT as i128
{
panic!("amounts too large for exact arithmetic (2^53 - 1)");
}
LtvCac {
ltv: if churn == 0 {
None
} else {
Some(money(round_div(margin, churn, "half-up"), &arpa.currency))
},
ltv_to_cac_basis_points: if churn == 0 || cac.minor == 0 {
None
} else {
Some(round_div(margin * 10000, churn * cac.minor, "half-up"))
},
payback_months: if margin == 0 {
None
} else {
Some(round_div(cac.minor * 10000, margin, "up"))
},
}
}
pub fn ltv_cac_to_value(r: &LtvCac) -> Value {
Value::obj(vec![
(
"ltv",
match &r.ltv {
Some(m) => money_to_value(m),
None => Value::Null,
},
),
(
"ltvToCacBasisPoints",
match r.ltv_to_cac_basis_points {
Some(n) => Value::Int(n),
None => Value::Null,
},
),
(
"paybackMonths",
match r.payback_months {
Some(n) => Value::Int(n),
None => Value::Null,
},
),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
ltv_cac_to_value(<v_cac(
&money_from_value(&args[0]),
args[1].as_i64(),
args[2].as_i64(),
&money_from_value(&args[3]),
))
}