Functional Weave
Code in Rust

charity.restricted-funds@1.0.0

impl/rust.rs

5,564 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::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn same_currency(currency: &str, amount: &Money) {
    if amount.currency != currency {
        panic!("currency mismatch: {} and {}", amount.currency, currency);
    }
}

/// Charge spending to funds the way charity fund accounting requires: a spend
/// for a restricted purpose uses that purpose's restricted funds first (in the
/// order given), and only what they cannot cover falls on the unrestricted
/// funds. General spending never touches a restricted fund, and no fund may go
/// below zero.
///
/// # Panics
/// Panics on mixed currencies, duplicate fund ids, a restricted fund without a
/// purpose or an unrestricted one with one, negative balances or spends, and a
/// spend the funds cannot cover.
pub fn allocate_fund_spend(funds: &[Fund], spends: &[FundSpend]) -> FundAllocation {
    let currency: String = if let Some(f) = funds.first() {
        f.balance.currency.clone()
    } else if let Some(s) = spends.first() {
        s.amount.currency.clone()
    } else {
        "GBP".to_string()
    };
    let mut seen: Vec<&str> = Vec::new();
    for fund in funds {
        if seen.contains(&fund.id.as_str()) {
            panic!("duplicate fund id \"{}\"", fund.id);
        }
        seen.push(&fund.id);
        same_currency(&currency, &fund.balance);
        if fund.restricted && fund.purpose.is_none() {
            panic!("restricted fund \"{}\" needs a purpose", fund.id);
        }
        if !fund.restricted && fund.purpose.is_some() {
            panic!("unrestricted fund \"{}\" must not have a purpose", fund.id);
        }
        if fund.balance.minor < 0 {
            panic!("fund \"{}\" balance must not be negative, received {}", fund.id, fund.balance.minor);
        }
    }
    for spend in spends {
        same_currency(&currency, &spend.amount);
        if spend.amount.minor < 0 {
            panic!("spend \"{}\" amount must not be negative, received {}", spend.id, spend.amount.minor);
        }
    }

    let mut balances: Vec<i64> = funds.iter().map(|f| f.balance.minor).collect();
    let mut draws: Vec<FundDraw> = Vec::new();
    for spend in spends {
        let mut remaining = spend.amount.minor;
        let mut order: Vec<usize> = Vec::new();
        if let Some(purpose) = &spend.purpose {
            order.extend((0..funds.len()).filter(|&i| funds[i].restricted && funds[i].purpose.as_ref() == Some(purpose)));
        }
        order.extend((0..funds.len()).filter(|&i| !funds[i].restricted));
        for i in order {
            let amount = remaining.min(balances[i]);
            if amount <= 0 {
                continue;
            }
            balances[i] -= amount;
            remaining -= amount;
            draws.push(FundDraw {
                spend_id: spend.id.clone(),
                fund_id: funds[i].id.clone(),
                amount: money(amount, &currency),
            });
        }
        if remaining > 0 {
            panic!("insufficient funds for spend \"{}\": short by {}", spend.id, remaining);
        }
    }
    FundAllocation {
        draws,
        funds: funds
            .iter()
            .enumerate()
            .map(|(i, f)| Fund {
                id: f.id.clone(),
                restricted: f.restricted,
                purpose: f.purpose.clone(),
                balance: money(balances[i], &currency),
            })
            .collect(),
    }
}

fn optional_str(v: &Value) -> Option<String> {
    if v.is_null() {
        None
    } else {
        Some(v.as_str().to_string())
    }
}

pub fn fund_from_value(v: &Value) -> Fund {
    Fund {
        id: v.get("id").as_str().to_string(),
        restricted: v.get("restricted").as_bool(),
        purpose: optional_str(v.get("purpose")),
        balance: money_from_value(v.get("balance")),
    }
}

pub fn fund_spend_from_value(v: &Value) -> FundSpend {
    FundSpend {
        id: v.get("id").as_str().to_string(),
        purpose: optional_str(v.get("purpose")),
        amount: money_from_value(v.get("amount")),
    }
}

pub fn fund_allocation_to_value(a: &FundAllocation) -> Value {
    Value::obj(vec![
        (
            "draws",
            Value::Arr(
                a.draws
                    .iter()
                    .map(|d| {
                        Value::obj(vec![
                            ("spendId", Value::str(&d.spend_id)),
                            ("fundId", Value::str(&d.fund_id)),
                            ("amount", money_to_value(&d.amount)),
                        ])
                    })
                    .collect(),
            ),
        ),
        (
            "funds",
            Value::Arr(
                a.funds
                    .iter()
                    .map(|f| {
                        Value::obj(vec![
                            ("id", Value::str(&f.id)),
                            ("restricted", Value::Bool(f.restricted)),
                            ("purpose", match &f.purpose { Some(p) => Value::str(p), None => Value::Null }),
                            ("balance", money_to_value(&f.balance)),
                        ])
                    })
                    .collect(),
            ),
        ),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let funds: Vec<Fund> = args[0].as_arr().iter().map(fund_from_value).collect();
    let spends: Vec<FundSpend> = args[1].as_arr().iter().map(fund_spend_from_value).collect();
    fund_allocation_to_value(&allocate_fund_spend(&funds, &spends))
}