use super::funejson::Value; use super::math_round_div::round_div; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money}; 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]), )) }