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.
def allocate_fund_spend(funds: Sequence[Fund], spends: Sequence[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
@dataclass(frozen=True)
class Fund:
"""A fund and its balance."""
id: str
restricted: bool
#: what a restricted fund may be spent on; null for an unrestricted fund
purpose: Optional[str]
balance: Money
@dataclass(frozen=True)
class FundSpend:
"""One item of expenditure."""
id: str
#: the restricted purpose it serves, or null for general spending
purpose: Optional[str]
amount: Money
@dataclass(frozen=True)
class FundDraw:
"""Part of a spend charged to one fund."""
spend_id: str
fund_id: str
amount: Money
@dataclass(frozen=True)
class FundAllocation:
"""The draws, and the funds with their closing balances."""
draws: List[FundDraw]
funds: List[Fund]
Your code names it in one line, in the file that uses it
from fune.charity.restricted_funds import allocate_fund_spend # charity.restricted-funds@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import List, Sequence
from .charity_restricted_funds_types import Fund, FundAllocation, FundDraw, FundSpend
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
def _same_currency(currency: str, amount: Money) -> None:
if amount.currency != currency:
raise ValueError("currency mismatch: %s and %s" % (amount.currency, currency))
def allocate_fund_spend(funds: Sequence[Fund], spends: Sequence[FundSpend]) -> FundAllocation:
"""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.
"""
if len(funds) > 0:
currency = funds[0].balance.currency
elif len(spends) > 0:
currency = spends[0].amount.currency
else:
currency = "GBP"
seen = set()
for fund in funds:
if fund.id in seen:
raise ValueError('duplicate fund id "%s"' % (fund.id,))
seen.add(fund.id)
_same_currency(currency, fund.balance)
if fund.restricted and fund.purpose is None:
raise ValueError('restricted fund "%s" needs a purpose' % (fund.id,))
if not fund.restricted and fund.purpose is not None:
raise ValueError('unrestricted fund "%s" must not have a purpose' % (fund.id,))
if fund.balance.minor < 0:
raise ValueError('fund "%s" balance must not be negative, received %d' % (fund.id, fund.balance.minor))
for spend in spends:
_same_currency(currency, spend.amount)
if spend.amount.minor < 0:
raise ValueError('spend "%s" amount must not be negative, received %d' % (spend.id, spend.amount.minor))
balances = [f.balance.minor for f in funds]
draws: List[FundDraw] = []
for spend in spends:
remaining = spend.amount.minor
order = []
if spend.purpose is not None:
order += [i for i, f in enumerate(funds) if f.restricted and f.purpose == spend.purpose]
order += [i for i, f in enumerate(funds) if not f.restricted]
for i in order:
amount = min(remaining, balances[i])
if amount <= 0:
continue
balances[i] -= amount
remaining -= amount
draws.append(FundDraw(spend_id=spend.id, fund_id=funds[i].id, amount=money(amount, currency)))
if remaining > 0:
raise ValueError('insufficient funds for spend "%s": short by %d' % (spend.id, remaining))
return FundAllocation(
draws=draws,
funds=[
Fund(id=f.id, restricted=f.restricted, purpose=f.purpose, balance=money(balances[i], currency))
for i, f in enumerate(funds)
],
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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. 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 Python implementation. Install it without the registry with fune add ./charity.restricted-funds-1.0.0-python.fune, or fetch it from a terminal with fune pull charity.restricted-funds@1.0.0:python.
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 |