Functional Weave
Code in TypeScript

subscriptions.ltv-cac@1.0.0

impl/rust.rs

2,937 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, 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(&ltv_cac(
        &money_from_value(&args[0]),
        args[1].as_i64(),
        args[2].as_i64(),
        &money_from_value(&args[3]),
    ))
}