use super::funejson::Value; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money}; use super::money_sum::sum_money; fn charge(tier: i64, quantity: i64, t: &UsageTier) -> TierCharge { TierCharge { tier, quantity, unit_price: t.unit_price.clone(), flat_fee: t.flat_fee.clone(), amount: money(quantity * t.unit_price.minor + t.flat_fee.minor, &t.unit_price.currency), } } /// Price `quantity` units against ascending tiers. Graduated prices each unit /// in its own tier and charges the flat fee of every tier reached; volume /// prices every unit at the tier the quantity falls in, plus that tier's flat /// fee. The first tier is always reached, so its flat fee applies at zero. /// /// # Panics /// Panics on a negative quantity, an unknown mode, no tiers, badly ordered /// tiers, usage beyond a closed last tier, or mixed currencies. pub fn price_usage(quantity: i64, tiers: &[UsageTier], mode: &str) -> UsageCharge { if quantity < 0 { panic!("quantity must be a whole number of 0 or more, received {}", quantity); } if mode != "graduated" && mode != "volume" { panic!("unknown tier mode \"{}\": expected graduated or volume", mode); } if tiers.is_empty() { panic!("tiered pricing needs at least one tier"); } let currency = tiers[0].unit_price.currency.clone(); let mut previous = 0; for (i, t) in tiers.iter().enumerate() { assert_same_currency(&tiers[0].unit_price, &t.unit_price); assert_same_currency(&tiers[0].unit_price, &t.flat_fee); match t.up_to { None => { if i != tiers.len() - 1 { panic!("only the last tier may be open-ended (upTo null)"); } } Some(up_to) => { if up_to <= previous { panic!( "tier upTo values must be positive and strictly increasing, received {} after {}", up_to, previous ); } previous = up_to; } } } if let Some(end) = tiers[tiers.len() - 1].up_to { if quantity > end { panic!("quantity {} exceeds the last tier, which ends at {}", quantity, end); } } let mut lines: Vec = Vec::new(); if mode == "volume" { let index = tiers .iter() .position(|t| t.up_to.map_or(true, |end| quantity <= end)) .unwrap(); lines.push(charge(index as i64 + 1, quantity, &tiers[index])); } else { let mut floor = 0; for (i, t) in tiers.iter().enumerate() { // Tier 1 is always reached; a later tier only once usage passes // the previous tier's last unit. if i > 0 && quantity <= floor { break; } let top = t.up_to.map_or(quantity, |end| quantity.min(end)); lines.push(charge(i as i64 + 1, top - floor, t)); match t.up_to { None => break, Some(end) => floor = end, } } } let amounts: Vec = lines.iter().map(|l| l.amount.clone()).collect(); let total = sum_money(&amounts, ¤cy); UsageCharge { lines, total } } pub fn usage_tier_from_value(v: &Value) -> UsageTier { UsageTier { up_to: if v.get("upTo").is_null() { None } else { Some(v.get("upTo").as_i64()) }, unit_price: money_from_value(v.get("unitPrice")), flat_fee: money_from_value(v.get("flatFee")), } } pub fn usage_charge_to_value(c: &UsageCharge) -> Value { Value::obj(vec![ ( "lines", Value::Arr( c.lines .iter() .map(|l| { Value::obj(vec![ ("tier", Value::Int(l.tier)), ("quantity", Value::Int(l.quantity)), ("unitPrice", money_to_value(&l.unit_price)), ("flatFee", money_to_value(&l.flat_fee)), ("amount", money_to_value(&l.amount)), ]) }) .collect(), ), ), ("total", money_to_value(&c.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let tiers: Vec = args[1].as_arr().iter().map(usage_tier_from_value).collect(); usage_charge_to_value(&price_usage(args[0].as_i64(), &tiers, args[2].as_str())) }