Functional Weave
Code in TypeScript

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 fund
  • allocateFundSpend(funds ×3, spends ×1) → draws ×2, funds ×3 the restricted fund is used up first, the rest falls on unrestricted funds
  • allocateFundSpend(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
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. */
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";
impl/typescript.ts · 61 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.

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
Download for TypeScript charity.restricted-funds-1.0.0-typescript.fune · 20,993 bytes sha256 abd94d15648961a23e08e51cd61631bb8af18b9ec4029275c3e918bb8ca28223

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.

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