Functional Weave
Code in Rust

charity.restricted-funds@1.0.0

impl/python.py

2,858 bytes · the Python implementation · view 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)
        ],
    )