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
allocateFundSpend(funds ×3, spends ×1)→ draws ×1, funds ×3 an earmarked spend is charged to its restricted fundallocateFundSpend(funds ×3, spends ×1)→ draws ×2, funds ×3 the restricted fund is used up first, the rest falls on unrestricted fundsallocateFundSpend(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.
export function allocateFundSpend(funds: readonly Fund[], spends: readonly 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. */
export interface Fund {
readonly id: string;
readonly restricted: boolean;
/** what a restricted fund may be spent on; null for an unrestricted fund */
readonly purpose: string | null;
readonly balance: Money;
}
/** One item of expenditure. */
export interface FundSpend {
readonly id: string;
/** the restricted purpose it serves, or null for general spending */
readonly purpose: string | null;
readonly amount: Money;
}
/** Part of a spend charged to one fund. */
export interface FundDraw {
readonly spendId: string;
readonly fundId: string;
readonly amount: Money;
}
/** The draws, and the funds with their closing balances. */
export interface FundAllocation {
readonly draws: readonly FundDraw[];
readonly funds: readonly Fund[];
}
Your code names it in one line, in the file that uses it
import { allocateFundSpend } from "#fune/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.
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type Fund, type FundAllocation, type FundDraw, type FundSpend } from "./charity_restricted_funds_types.ts";
function sameCurrency(currency: string, amount: Money): void {
if (amount.currency !== currency) throw new RangeError(`currency mismatch: ${amount.currency} and ${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: spending that the funds cannot cover is refused, not recorded
* as a deficit on a restricted fund.
*/
export function allocateFundSpend(funds: readonly Fund[], spends: readonly FundSpend[]): FundAllocation {
const currency = funds.length > 0 ? funds[0].balance.currency : spends.length > 0 ? spends[0].amount.currency : "GBP";
const seen = new Set<string>();
for (const fund of funds) {
if (seen.has(fund.id)) throw new RangeError(`duplicate fund id "${fund.id}"`);
seen.add(fund.id);
sameCurrency(currency, fund.balance);
if (fund.restricted && fund.purpose === null) throw new RangeError(`restricted fund "${fund.id}" needs a purpose`);
if (!fund.restricted && fund.purpose !== null) {
throw new RangeError(`unrestricted fund "${fund.id}" must not have a purpose`);
}
if (fund.balance.minor < 0) throw new RangeError(`fund "${fund.id}" balance must not be negative, received ${fund.balance.minor}`);
}
for (const spend of spends) {
sameCurrency(currency, spend.amount);
if (spend.amount.minor < 0) {
throw new RangeError(`spend "${spend.id}" amount must not be negative, received ${spend.amount.minor}`);
}
}
const balances = funds.map((f) => f.balance.minor);
const draws: FundDraw[] = [];
for (const spend of spends) {
let remaining = spend.amount.minor;
const take = (i: number): void => {
const amount = Math.min(remaining, balances[i]);
if (amount <= 0) return;
balances[i] -= amount;
remaining -= amount;
draws.push({ spendId: spend.id, fundId: funds[i].id, amount: money(amount, currency) });
};
if (spend.purpose !== null) {
funds.forEach((f, i) => {
if (f.restricted && f.purpose === spend.purpose) take(i);
});
}
funds.forEach((f, i) => {
if (!f.restricted) take(i);
});
if (remaining > 0) throw new RangeError(`insufficient funds for spend "${spend.id}": short by ${remaining}`);
}
return {
draws,
funds: funds.map((f, i) => ({ id: f.id, restricted: f.restricted, purpose: f.purpose, balance: money(balances[i], currency) })),
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript 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 TypeScript implementation. Install it without the registry with fune add ./charity.restricted-funds-1.0.0-typescript.fune, or fetch it from a terminal with fune pull charity.restricted-funds@1.0.0:typescript.
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 |