use super::funejson::Value; use super::math_rational::{add_rational, multiply_rational, rational, rational_to_integer}; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value}; /// MRR and ARR from active and past-due subscriptions. Every charge becomes an /// exact annual amount first, and each total is rounded once at the end, so a /// list of annual plans does not lose a penny per plan. /// /// # Panics /// Panics on a currency mismatch, a negative amount, an interval count below /// 1, or an unknown interval or status. pub fn mrr_arr(subscriptions: &[RecurringCharge], currency: &str) -> MrrArr { let zero = money(0, currency); let mut annual = rational(0, 1); let mut counted = 0; let mut excluded = 0; for s in subscriptions { assert_same_currency(&zero, &s.amount); if s.amount.minor < 0 { panic!("recurring amount must not be negative, received {}", s.amount.minor); } if s.interval_count < 1 { panic!("interval count must be 1 or more, received {}", s.interval_count); } let per_year: i64 = match s.interval.as_str() { "day" => 365, "week" => 52, "month" => 12, "quarter" => 4, "year" => 1, other => panic!( "unknown billing interval \"{}\": expected day, week, month, quarter or year", other ), }; match s.status.as_str() { "active" | "past-due" => { counted += 1; annual = add_rational(&annual, &rational(s.amount.minor * per_year, s.interval_count)); } "trialing" | "paused" | "canceled" => excluded += 1, other => panic!( "unknown subscription status \"{}\": expected active, past-due, trialing, paused or canceled", other ), } } MrrArr { mrr: money(rational_to_integer(&multiply_rational(&annual, &rational(1, 12)), "half-up"), currency), arr: money(rational_to_integer(&annual, "half-up"), currency), counted, excluded, } } pub fn recurring_charge_from_value(v: &Value) -> RecurringCharge { RecurringCharge { amount: money_from_value(v.get("amount")), interval: v.get("interval").as_str().to_string(), interval_count: v.get("intervalCount").as_i64(), status: v.get("status").as_str().to_string(), } } pub fn mrr_arr_to_value(r: &MrrArr) -> Value { Value::obj(vec![ ("mrr", money_to_value(&r.mrr)), ("arr", money_to_value(&r.arr)), ("counted", Value::Int(r.counted)), ("excluded", Value::Int(r.excluded)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let subs: Vec = args[0].as_arr().iter().map(recurring_charge_from_value).collect(); mrr_arr_to_value(&mrr_arr(&subs, args[1].as_str())) }