Functional Weave
Code in Python

subscriptions.churn@1.0.0

impl/rust.rs

3,098 bytes · the Rust implementation · view raw

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_from_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn rate(numerator: i64, denominator: i64) -> Option<i64> {
    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<i64>) -> 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]),
    ))
}