Functional Weave
Code in Python

hospitality.tronc-allocation

Share a tips pool (tronc) between staff by hours worked, points, or hours times points, exactly to the penny.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 19 tests, run in TypeScript, Python and Rust.

What it does

Shares a pool of tips, gratuities and service charges between the people in it, in proportion to one of three weights:

- `hours`: minutes worked in the period; - `points`: each person's points under the venue's tipping policy (for example head chef 10, commis 4, kitchen porter 3); - `hours-and-points`: minutes worked times points, the usual "points per hour" scheme.

For example

  • tronc_allocation(£1,000.00, staff ×4, hours) → ×4 by hours: 40, 30, 20 and 10 hours share 1000.00
  • tronc_allocation(£100.00, staff ×3, hours) → ×3 equal hours, the odd penny goes to the first listed
  • tronc_allocation(£500.00, staff ×4, points) → ×4 by points: 10, 7, 4 and 3 points share 500.00 without losing a penny

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 tronc_allocation(pool: Money, staff: Sequence[TroncMember], method: TroncMethod) -> List[TroncShare]
poolMoneythe tips, gratuities and service charges to share, after nothing but tax
staffTroncMember[]everyone in the pool for the period, at least one
methodTroncMethodhours, points, or hours-and-points (minutes x points)
returnsTroncShare[]one share per member, in the order given; the shares add up to the pool exactly

The types it declares, generated into your project

@dataclass(frozen=True)
class TroncMember:
    """One person in the pool."""

    #: unique within the pool
    id: str
    #: time worked in the period, in minutes
    minutes_worked: int
    #: the member's points under the written tipping policy
    points: int

TroncMethod = Literal["hours", "points", "hours-and-points"]

@dataclass(frozen=True)
class TroncShare:
    """What one person receives."""

    id: str
    #: the number the pool was shared by: minutes, points, or minutes x points
    weight: int
    share: Money

Your code names it in one line, in the file that uses it

from fune.hospitality.tronc_allocation import tronc_allocation  # hospitality.tronc-allocation@^1
impl/python.py · 52 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 .hospitality_tronc_allocation_types import TroncMember, TroncMethod, TroncShare
from .money_allocate import allocate  ← from money.allocate ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune


def _whole(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def _weight_of(member: TroncMember, method: str) -> int:
    if method == "hours":
        return member.minutes_worked
    if method == "points":
        return member.points
    if method == "hours-and-points":
        return member.minutes_worked * member.points
    raise ValueError('unknown tronc method "%s"' % (method,))


def tronc_allocation(pool: Money, staff: Sequence[TroncMember], method: TroncMethod) -> List[TroncShare]:
    """Share a tips pool between staff in proportion to a weight.

    Time is in minutes so a 7.5-hour shift is exact. The pool is split with
    money.allocate, so the shares add up to the pool exactly: the leftover
    pennies go to the largest remainders, ties to whoever is listed first.
    """
    if len(staff) == 0:
        raise ValueError("staff must have at least one member")
    if pool.minor < 0:
        raise ValueError("pool must not be negative, received %d" % (pool.minor,))
    ids = set()
    for member in staff:
        if member.id == "":
            raise ValueError("every member needs an id")
        if member.id in ids:
            raise ValueError('staff id "%s" appears twice' % (member.id,))
        ids.add(member.id)
        if not _whole(member.minutes_worked) or member.minutes_worked < 0:
            raise ValueError(
                'minutesWorked must not be negative, received %s for "%s"' % (member.minutes_worked, member.id)
            )
        if not _whole(member.points) or member.points < 0:
            raise ValueError('points must not be negative, received %s for "%s"' % (member.points, member.id))
    weights = [_weight_of(member, method) for member in staff]
    if sum(weights) == 0:
        if pool.minor != 0:
            raise ValueError("nobody in the pool has any weight to share it by")
        return [TroncShare(id=member.id, weight=0, share=money(0, pool.currency)) for member in staff]
    shares = allocate(pool, weights)
    return [TroncShare(id=member.id, weight=weights[i], share=shares[i]) for i, member in enumerate(staff)]

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 dependencies, 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 hospitality.tronc-allocation
Download for Python hospitality.tronc-allocation-1.0.0-python.fune · 17,302 bytes sha256 f9c0e9aa8973aeeacd890f4f216060f7f274ca451931e92fda6f77bb22bfaa4a

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./hospitality.tronc-allocation-1.0.0-python.fune, or fetch it from a terminal with fune pull hospitality.tronc-allocation@1.0.0:python.

The whole function, every language, is one file too: hospitality.tronc-allocation-1.0.0.fune, 23,042 bytes, sha256 2a06249aa9fe592eb58b2943d4d639adb5cac80e12b5c5743c97efbeab417afe. 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 hospitality.tronc-allocation

after — your function gets the result and the arguments, and returns the final result.

# fune: after hospitality.tronc-allocation

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.allocate in hospitality.tronc-allocation
# fune: replace money.amount in hospitality.tronc-allocation

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 hospitality.tronc-allocation --steps.

# fune: step hospitality.tronc-allocation 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
by hours: 40, 30, 20 and 10 hours share 1000.00 £1,000.00, staff ×4, hours → ×4
equal hours, the odd penny goes to the first listed £100.00, staff ×3, hours → ×3
by points: 10, 7, 4 and 3 points share 500.00 without losing a penny £500.00, staff ×4, points → ×4
hours and points: minutes worked times points £840.00, staff ×3, hours-and-points → ×3
hours and points on the floor team £1,000.00, staff ×4, hours-and-points → ×4
a 7.5 hour shift is exact in minutes £10.00, staff ×2, hours → ×2
someone who worked no hours gets nothing £10.01, staff ×2, hours → ×2
the leftover pennies go to the largest remainders, not the first listed £1.00, staff ×4, hours → ×4
an empty pool shares nothing £0.00, staff ×4, hours → ×4
an empty pool with nobody weighted is still nothing £0.00, staff ×1, hours → ×1
Show the other 9 tests
CaseArgumentsExpected
one person takes the whole pool €123.45, staff ×1, points → ×1
no staff is an error £1.00, , hours → error: staff must have at least one member
a negative pool is an error -£1.00, staff ×4, hours → error: pool must not be negative
a duplicate id is an error £1.00, staff ×2, hours → error: staff id "a" appears twice
negative minutes are an error £1.00, staff ×1, hours → error: minutesWorked must not be negative
negative points are an error £1.00, staff ×1, points → error: points must not be negative
a pool with nobody to share it by is an error £1.00, staff ×2, points → error: nobody in the pool has any weight to share it by
an unknown method is an error £1.00, staff ×4, seniority → error: unknown tronc method "seniority"
an empty id is an error £1.00, staff ×1, hours → error: every member needs an id

More from the author

The shares always add up to the pool exactly. The split is `money.allocate`: each person gets their proportional share rounded down, and the pennies left over go one at a time to the largest remainders, ties to whoever is listed first. So the same inputs always give the same split, and nobody's share is nudged by rounding more than a penny.

## Why minutes

Hours are often fractional (a 7.5-hour shift), and a fraction of an hour as a float is exactly the kind of drift this registry avoids. Minutes are whole numbers, and nothing smaller matters for a tronc.

## Edge cases

- Someone with a weight of 0 (no hours, or no points) receives 0. - An empty pool gives everyone 0. A pool with money in it but no weight on anybody is an error, because there is no fair way to split it. - Ids must be unique and not empty; minutes and points must not be negative.

## The law this does not enforce

Since 1 October 2024 the **Employment (Allocation of Tips) Act 2023** (https://www.legislation.gov.uk/ukpga/2023/13/contents), which inserted Part 2B into the Employment Rights Act 1996, requires employers in Great Britain to pass on all tips, gratuities and service charges they control or significantly influence to workers, without deductions other than tax, and to do so fairly. The **Code of Practice on Fair and Transparent Distribution of Tips**, brought into force by SI 2024/831 (https://www.legislation.gov.uk/uksi/2024/831/made; guidance at https://www.gov.uk/government/publications/distributing-tips-fairly-revised-statutory-code-of-practice and https://www.acas.org.uk/tips-and-service-charges), sets out what fair means in practice. A revised draft Code was published on 29 June 2026 and withdrawn on 29 July 2026 pending consultation, so the 2024 Code still applies.

This function does the arithmetic of a policy. It cannot check that the policy is lawful. In particular, the employer is responsible for:

- allocating fairly (ERA 1996 s27D): choosing fair factors and weights and applying them consistently. The Code discusses factors such as role, hours, seniority and performance; - **not** keeping a share for the business. An employer who is an individual and works alongside staff may only take part as the Act and the Code allow; - including agency workers who worked at the venue; - paying tips no later than the end of the month after the month they were paid (s27G); - having a written tipping policy available to workers (s27I), keeping records of the tips and how they were shared for three years, and answering a worker's written request for them within four weeks (s27J); - consulting workers on the tipping policy, a duty the Employment Rights Act 2025 adds. Check what is in force on the date you use this.

Tax and National Insurance on tronc payments (whether a troncmaster scheme takes the payments out of Class 1 NICs) are payroll matters and are not done here.

Files

PathBytes
README.md3,317
impl/python.py2,336
impl/rust.rs3,239
impl/typescript.ts2,260
vectors.json7,388