charity.restricted-funds
Allocate charity spending to restricted funds first, then unrestricted funds, never overdrawing a restricted fund.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 17 tests, run in TypeScript, Python and Rust.
What it does
Charge a list of spending to a charity's funds under the basic rule of fund accounting: money given for a particular purpose (a restricted fund) may only be spent on that purpose, and general spending comes out of unrestricted funds.
For each spend, in the order given:
For example
allocate_fund_spend(funds ×3, spends ×1)→ draws ×1, funds ×3 an earmarked spend is charged to its restricted fundallocate_fund_spend(funds ×3, spends ×1)→ draws ×2, funds ×3 the restricted fund is used up first, the rest falls on unrestricted fundsallocate_fund_spend(funds ×3, spends ×1)→ draws ×1, funds ×3 general spending never touches a restricted fund
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn allocate_fund_spend(funds: &[Fund], spends: &[FundSpend]) -> FundAllocation
| funds | Fund[] | opening balances; restricted funds name the purpose they may be spent on |
| spends | FundSpend[] | in the order they are to be charged; a spend with a purpose is charged to that purpose's restricted funds first |
| returns | FundAllocation | every draw in order, and each fund's closing balance in the order given |
The types it declares, generated into your project
/// A fund and its balance.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fund {
pub id: String,
pub restricted: bool,
/// what a restricted fund may be spent on; null for an unrestricted fund
pub purpose: Option<String>,
pub balance: Money,
}
/// One item of expenditure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FundSpend {
pub id: String,
/// the restricted purpose it serves, or null for general spending
pub purpose: Option<String>,
pub amount: Money,
}
/// Part of a spend charged to one fund.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FundDraw {
pub spend_id: String,
pub fund_id: String,
pub amount: Money,
}
/// The draws, and the funds with their closing balances.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FundAllocation {
pub draws: Vec<FundDraw>,
pub funds: Vec<Fund>,
}
Your code names it in one line, in the file that uses it
fune!(charity.restricted-funds@^1); // then call allocate_fund_spend(…)
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))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add charity.restricted-funds
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./charity.restricted-funds-1.0.0-rust.fune, or fetch it from a terminal with fune pull charity.restricted-funds@1.0.0:rust.
The whole function, every language, is one file too: charity.restricted-funds-1.0.0.fune, 29,754 bytes, sha256 ce887799e858a67e88f6ddc95c98896573e2f59abebe531670db8ee6efe8287d. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before charity.restricted-funds
after — your function gets the result and the arguments, and returns the final result.
// fune: after charity.restricted-funds
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace money.amount in charity.restricted-funds
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show charity.restricted-funds --steps.
// fune: step charity.restricted-funds after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| an earmarked spend is charged to its restricted fund | funds ×3, spends ×1 | → | draws ×1, funds ×3 |
| the restricted fund is used up first, the rest falls on unrestricted funds | funds ×3, spends ×1 | → | draws ×2, funds ×3 |
| general spending never touches a restricted fund | funds ×3, spends ×1 | → | draws ×1, funds ×3 |
| general spending beyond the unrestricted funds is refused even though restricted money sits unused | funds ×3, spends ×1 | → | error: insufficient funds for spend "rent": short by 1 |
| spends are charged in order: the second earmarked spend finds the fund partly used | funds ×3, spends ×2 | → | draws ×3, funds ×3 |
| a purpose with no restricted fund is paid from unrestricted funds | funds ×3, spends ×1 | → | draws ×1, funds ×3 |
| two restricted funds for one purpose are used in the order given, then two unrestricted funds | funds ×4, spends ×1 | → | draws ×4, funds ×4 |
| a spend using every penny exactly is allowed | funds ×2, spends ×1 | → | draws ×2, funds ×2 |
| a zero spend draws nothing | funds ×3, spends ×1 | → | draws , funds ×3 |
| nothing to allocate | , | → | draws , funds |
Show the other 7 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| an earmarked overspend that unrestricted funds cannot cover is refused | funds ×2, spends ×1 | → | error: insufficient funds for spend "s": short by 1 |
| a restricted fund without a purpose is an error | funds ×1, | → | error: restricted fund "r" needs a purpose |
| an unrestricted fund with a purpose is an error | funds ×1, | → | error: unrestricted fund "g" must not have a purpose |
| duplicate fund ids are an error | funds ×2, | → | error: duplicate fund id "g" |
| a negative fund balance is an error | funds ×1, | → | error: fund "g" balance must not be negative |
| a negative spend is an error | funds ×1, spends ×1 | → | error: spend "s" amount must not be negative |
| mixed currencies are an error | funds ×1, spends ×1 | → | error: currency mismatch: EUR and GBP |
More from the author
1. a spend with a `purpose` is charged to the restricted funds for that purpose, in the order the funds are listed, as far as their balances go; 2. whatever is left, and every spend without a purpose, is charged to the unrestricted funds, in the order listed; 3. if that still does not cover it, the whole allocation is refused with `insufficient funds for spend "<id>": short by <minor units>`.
So a restricted fund is never overdrawn and never used for anything else, even when it holds money that would cover a general bill. The result lists every draw (spend, fund, amount) in the order made, and every fund with its closing balance in the order given.
## Decisions
- **Refuse rather than record a deficit.** A restricted fund in deficit is a real finding in charity accounts (it usually means unrestricted money has to make it good), so a deficit is never created silently: the caller sees the shortfall and decides. - **Order is the caller's.** Which restricted fund is used first when several share a purpose, and which unrestricted fund pays first, follow the lists as given, so the answer is deterministic and the policy stays visible. - An unrestricted fund may not carry a purpose. Designated funds (unrestricted money the trustees have set aside) are a management choice, not a legal restriction; model them as their own restricted-like purpose only if your policy treats them that way. - Amounts are integer minor units, all in one currency.
## Background
The Charities SORP (FRS 102) requires restricted and unrestricted funds to be accounted for separately, and spending on a restricted purpose to be charged to the restricted fund. This capability applies that rule mechanically; it does not decide whether an item of spending falls within a fund's purpose.
Files
| Path | Bytes |
|---|---|
| README.md | 2,104 |
| impl/python.py | 2,858 |
| impl/rust.rs | 5,564 |
| impl/typescript.ts | 2,804 |
| vectors.json | 10,887 |