use super::funejson::Value; use super::dates_add_days::epoch_day_from_iso; use super::money_amount::{money, money_to_value}; use super::money_sum::sum_money; use super::telecoms_roaming_charge_data::{RoamingRate, ROAMING_TARIFFS, ROAMING_TARIFFS_HISTORY}; fn is_country(s: &str) -> bool { s.len() == 2 && s.bytes().all(|b| b.is_ascii_uppercase()) } /// Price roaming usage in one country on one date: find the tariff's zone for /// the country in force on that date, then multiply each kind of usage by its /// price. Exact integer arithmetic throughout. /// /// # Panics /// Panics on a malformed country or date, negative usage, or when no zone of /// the tariff covers the country on that date. pub fn roaming_charge(tariff: &str, country: &str, on_date: &str, usage: &RoamingUsage) -> RoamingCharge { if !is_country(country) { panic!( "country must be an ISO 3166-1 alpha-2 code in capitals, received \"{}\"", country ); } epoch_day_from_iso(on_date); let counts = [usage.days, usage.data_mb, usage.call_out_minutes, usage.call_in_minutes, usage.sms_out]; if counts.iter().any(|&n| n < 0) { panic!("usage must be whole numbers of 0 or more"); } let covers = |rule: &RoamingRate| rule.tariff == tariff && rule.countries.split(' ').any(|c| c == country); let mut best: Option<&RoamingRate> = None; for rule in ROAMING_TARIFFS { if !covers(rule) || on_date < rule.valid_from { continue; } if let Some(valid_to) = rule.valid_to { if on_date > valid_to { continue; } } if best.map_or(true, |b| rule.valid_from > b.valid_from) { best = Some(rule); } } let best = match best { Some(rule) => rule, None => { // A history=current build has dropped the rows that ended; // answering an older date with today's prices would be wrong, so // say what happened. if ROAMING_TARIFFS_HISTORY != "full" { let mut earliest: Option<&'static str> = None; for rule in ROAMING_TARIFFS { if covers(rule) && earliest.map_or(true, |e| rule.valid_from < e) { earliest = Some(rule.valid_from); } } if let Some(earliest) = earliest { if on_date < earliest { panic!( "no roaming zone for {} in tariff {} on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full to price older usage.", country, tariff, on_date, ROAMING_TARIFFS_HISTORY, earliest ); } } } panic!("no roaming zone for {} in tariff {} on {}", country, tariff, on_date) } }; let c = best.currency; let daily_fees = money(usage.days * best.daily_fee_minor, c); let data = money(usage.data_mb * best.data_per_mb_minor, c); let calls = money( usage.call_out_minutes * best.call_out_per_minute_minor + usage.call_in_minutes * best.call_in_per_minute_minor, c, ); let sms = money(usage.sms_out * best.sms_out_minor, c); let total = sum_money(&[daily_fees.clone(), data.clone(), calls.clone(), sms.clone()], c); RoamingCharge { tariff: tariff.to_string(), zone: best.zone.to_string(), daily_fees, data, calls, sms, total, } } pub fn roaming_usage_from_value(v: &Value) -> RoamingUsage { RoamingUsage { days: v.get("days").as_i64(), data_mb: v.get("dataMb").as_i64(), call_out_minutes: v.get("callOutMinutes").as_i64(), call_in_minutes: v.get("callInMinutes").as_i64(), sms_out: v.get("smsOut").as_i64(), } } pub fn roaming_charge_to_value(r: &RoamingCharge) -> Value { Value::obj(vec![ ("tariff", Value::str(&r.tariff)), ("zone", Value::str(&r.zone)), ("dailyFees", money_to_value(&r.daily_fees)), ("data", money_to_value(&r.data)), ("calls", money_to_value(&r.calls)), ("sms", money_to_value(&r.sms)), ("total", money_to_value(&r.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { roaming_charge_to_value(&roaming_charge( args[0].as_str(), args[1].as_str(), args[2].as_str(), &roaming_usage_from_value(&args[3]), )) }