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(¤cy, &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(¤cy, &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, ¤cy),
});
}
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], ¤cy),
})
.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))
}