Functional Weave
Code in Rust

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 fund
  • allocate_fund_spend(funds ×3, spends ×1) → draws ×2, funds ×3 the restricted fund is used up first, the rest falls on unrestricted funds
  • allocate_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
fundsFund[]opening balances; restricted funds name the purpose they may be spent on
spendsFundSpend[]in the order they are to be charged; a spend with a purpose is charged to that purpose's restricted funds first
returnsFundAllocationevery 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(…)
impl/rust.rs · 158 lines · open · 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))
}

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
Download for Rust charity.restricted-funds-1.0.0-rust.fune · 23,892 bytes sha256 ece5118ee7058bcb6df52823d22c17861bfcefa7e703c538af443fd562f101b7

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md2,104
impl/python.py2,858
impl/rust.rs5,564
impl/typescript.ts2,804
vectors.json10,887