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
troncAllocation(£1,000.00, staff ×4, hours)→ ×4 by hours: 40, 30, 20 and 10 hours share 1000.00troncAllocation(£100.00, staff ×3, hours)→ ×3 equal hours, the odd penny goes to the first listedtroncAllocation(£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.
export function troncAllocation(pool: Money, staff: readonly TroncMember[], method: TroncMethod): readonly 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
/** One person in the pool. */
export interface TroncMember {
/** unique within the pool */
readonly id: string;
/** time worked in the period, in minutes */
readonly minutesWorked: number;
/** the member's points under the written tipping policy */
readonly points: number;
}
export type TroncMethod = "hours" | "points" | "hours-and-points";
/** What one person receives. */
export interface TroncShare {
readonly id: string;
/** the number the pool was shared by: minutes, points, or minutes x points */
readonly weight: number;
readonly share: Money;
}
Your code names it in one line, in the file that uses it
import { troncAllocation } from "#fune/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.
import { allocate } from "./money_allocate.ts"; ← from money.allocate ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type TroncMember, type TroncMethod, type TroncShare } from "./hospitality_tronc_allocation_types.ts";
function weightOf(member: TroncMember, method: TroncMethod): number {
if (method === "hours") return member.minutesWorked;
if (method === "points") return member.points;
if (method === "hours-and-points") return member.minutesWorked * member.points;
throw new RangeError(`unknown tronc method "${method}"`);
}
/**
* 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.
*/
export function troncAllocation(pool: Money, staff: readonly TroncMember[], method: TroncMethod): readonly TroncShare[] {
if (staff.length === 0) throw new RangeError("staff must have at least one member");
if (pool.minor < 0) throw new RangeError(`pool must not be negative, received ${pool.minor}`);
const ids = new Set<string>();
for (const member of staff) {
if (member.id === "") throw new RangeError("every member needs an id");
if (ids.has(member.id)) throw new RangeError(`staff id "${member.id}" appears twice`);
ids.add(member.id);
if (!Number.isInteger(member.minutesWorked) || member.minutesWorked < 0) {
throw new RangeError(`minutesWorked must not be negative, received ${member.minutesWorked} for "${member.id}"`);
}
if (!Number.isInteger(member.points) || member.points < 0) {
throw new RangeError(`points must not be negative, received ${member.points} for "${member.id}"`);
}
}
const weights = staff.map((member) => weightOf(member, method));
const total = weights.reduce((sum, w) => sum + w, 0);
if (total === 0) {
if (pool.minor !== 0) throw new RangeError("nobody in the pool has any weight to share it by");
return staff.map((member) => ({ id: member.id, weight: 0, share: money(0, pool.currency) }));
}
const shares = allocate(pool, weights);
return staff.map((member, i) => ({ id: member.id, weight: weights[i], share: shares[i] }));
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, 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 hospitality.tronc-allocation
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./hospitality.tronc-allocation-1.0.0-typescript.fune, or fetch it from a terminal with fune pull hospitality.tronc-allocation@1.0.0:typescript.
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 |