Functional Weave
Code in Python

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.

def allocate_fund_spend(funds: Sequence[Fund], spends: Sequence[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

@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
impl/python.py · 65 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.

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
Download for Python charity.restricted-funds-1.0.0-python.fune · 21,047 bytes sha256 4cbdd00718cad114373535b6722b9f803be087d5daab8f75725934fd7f6daf43

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.

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