use super::funejson::Value; use super::math_round_div::round_div; use super::money_amount::{assert_same_currency, money_from_value, Money}; fn rate(numerator: i64, denominator: i64) -> Option { if denominator == 0 { None } else { Some(round_div(numerator * 10000, denominator, "half-up")) } } /// Logo churn, gross and net revenue churn, and gross and net revenue /// retention for one period, each as basis points of the starting cohort. /// /// # Panics /// Panics on negative counts or amounts, more lost than there was, or mixed /// currencies. pub fn churn_metrics( customers_at_start: i64, customers_lost: i64, mrr_at_start: &Money, expansion_mrr: &Money, contraction_mrr: &Money, churned_mrr: &Money, ) -> ChurnMetrics { if customers_at_start < 0 || customers_lost < 0 { panic!( "customer counts must be whole numbers of 0 or more, received {} and {}", customers_at_start, customers_lost ); } if customers_lost > customers_at_start { panic!( "customers lost must not exceed customers at start, received {} of {}", customers_lost, customers_at_start ); } for amount in [expansion_mrr, contraction_mrr, churned_mrr] { assert_same_currency(mrr_at_start, amount); } let start = mrr_at_start.minor; let expansion = expansion_mrr.minor; let lost = contraction_mrr.minor + churned_mrr.minor; if start < 0 || expansion < 0 || contraction_mrr.minor < 0 || churned_mrr.minor < 0 { panic!("MRR amounts must not be negative"); } if lost > start { panic!( "contraction and churned MRR must not exceed MRR at start, received {} of {}", lost, start ); } ChurnMetrics { logo_churn_basis_points: rate(customers_lost, customers_at_start), gross_revenue_churn_basis_points: rate(lost, start), net_revenue_churn_basis_points: rate(lost - expansion, start), gross_revenue_retention_basis_points: rate(start - lost, start), net_revenue_retention_basis_points: rate(start + expansion - lost, start), } } fn opt(v: Option) -> Value { match v { Some(n) => Value::Int(n), None => Value::Null, } } pub fn churn_metrics_to_value(m: &ChurnMetrics) -> Value { Value::obj(vec![ ("logoChurnBasisPoints", opt(m.logo_churn_basis_points)), ("grossRevenueChurnBasisPoints", opt(m.gross_revenue_churn_basis_points)), ("netRevenueChurnBasisPoints", opt(m.net_revenue_churn_basis_points)), ("grossRevenueRetentionBasisPoints", opt(m.gross_revenue_retention_basis_points)), ("netRevenueRetentionBasisPoints", opt(m.net_revenue_retention_basis_points)), ]) } pub fn fune_vector(args: &[Value]) -> Value { churn_metrics_to_value(&churn_metrics( args[0].as_i64(), args[1].as_i64(), &money_from_value(&args[2]), &money_from_value(&args[3]), &money_from_value(&args[4]), &money_from_value(&args[5]), )) }