Functional Weave
Code in Rust

subscriptions.usage-tiered@1.0.0

impl/rust.rs

4,621 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::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_sum::sum_money;  ← from money.sum ^1.0.0 · built alongside by fune

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<TierCharge> = 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<Money> = lines.iter().map(|l| l.amount.clone()).collect();
    let total = sum_money(&amounts, &currency);
    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<UsageTier> = 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()))
}