use std::collections::HashSet; 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}; fn is_digits(s: &str) -> bool { !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) } /// Rate one call: longest-prefix match on the dialled number, duration rounded /// up to the rate's billing increments, per-minute charge rounded once by /// `mode`, connection fee added and the minimum charge applied. /// /// # Panics /// Panics on a malformed number, a negative duration, a bad rate deck, no /// matching prefix, or an unknown rounding mode. pub fn rate_call(dialled_number: &str, duration_seconds: i64, rates: &[CallRate], mode: &str) -> RatedCall { let number = dialled_number.strip_prefix('+').unwrap_or(dialled_number); if !is_digits(number) { panic!( "dialled number must be international format digits, optionally with a leading +, received \"{}\"", dialled_number ); } if duration_seconds < 0 { panic!("duration must be whole seconds of 0 or more, received {}", duration_seconds); } let mut seen: HashSet<&str> = HashSet::new(); let mut best: Option<&CallRate> = None; for rate in rates { if !is_digits(&rate.prefix) { panic!("rate prefix must be digits, received \"{}\"", rate.prefix); } if !seen.insert(rate.prefix.as_str()) { panic!("duplicate prefix {} in the rate deck", rate.prefix); } if rate.first_increment_seconds < 1 || rate.next_increment_seconds < 1 { panic!( "billing increments must be 1 second or more, received {}/{} for prefix {}", rate.first_increment_seconds, rate.next_increment_seconds, rate.prefix ); } assert_same_currency(&rates[0].per_minute, &rate.per_minute); assert_same_currency(&rates[0].per_minute, &rate.connection_fee); assert_same_currency(&rates[0].per_minute, &rate.minimum_charge); if number.starts_with(rate.prefix.as_str()) && best.map_or(true, |b| rate.prefix.len() > b.prefix.len()) { best = Some(rate); } } let best = match best { Some(rate) => rate, None => panic!("no rate for dialled number {}", number), }; let currency = best.per_minute.currency.as_str(); if duration_seconds == 0 { return RatedCall { prefix: best.prefix.clone(), destination: best.destination.clone(), billed_seconds: 0, usage_charge: money(0, currency), connection_fee: money(0, currency), total: money(0, currency), }; } let first = best.first_increment_seconds; let step = best.next_increment_seconds; let billed_seconds = if duration_seconds <= first { first } else { first + ((duration_seconds - first + step - 1) / step) * step }; let usage = round_div(best.per_minute.minor * billed_seconds, 60, mode); let total = (usage + best.connection_fee.minor).max(best.minimum_charge.minor); RatedCall { prefix: best.prefix.clone(), destination: best.destination.clone(), billed_seconds, usage_charge: money(usage, currency), connection_fee: best.connection_fee.clone(), total: money(total, currency), } } pub fn call_rate_from_value(v: &Value) -> CallRate { CallRate { prefix: v.get("prefix").as_str().to_string(), destination: v.get("destination").as_str().to_string(), per_minute: money_from_value(v.get("perMinute")), connection_fee: money_from_value(v.get("connectionFee")), minimum_charge: money_from_value(v.get("minimumCharge")), first_increment_seconds: v.get("firstIncrementSeconds").as_i64(), next_increment_seconds: v.get("nextIncrementSeconds").as_i64(), } } pub fn rated_call_to_value(c: &RatedCall) -> Value { Value::obj(vec![ ("prefix", Value::str(&c.prefix)), ("destination", Value::str(&c.destination)), ("billedSeconds", Value::Int(c.billed_seconds)), ("usageCharge", money_to_value(&c.usage_charge)), ("connectionFee", money_to_value(&c.connection_fee)), ("total", money_to_value(&c.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let rates: Vec = args[2].as_arr().iter().map(call_rate_from_value).collect(); rated_call_to_value(&rate_call(args[0].as_str(), args[1].as_i64(), &rates, args[3].as_str())) }