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.00tronc_allocation(£100.00, staff ×3, hours)→ ×3 equal hours, the odd penny goes to the first listedtronc_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]
| pool | Money | the tips, gratuities and service charges to share, after nothing but tax |
| staff | TroncMember[] | everyone in the pool for the period, at least one |
| method | TroncMethod | hours, points, or hours-and-points (minutes x points) |
| returns | TroncShare[] | 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
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
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 3,317 |
| impl/python.py | 2,336 |
| impl/rust.rs | 3,239 |
| impl/typescript.ts | 2,260 |
| vectors.json | 7,388 |