Tip Pool: Fair Splits at the End of the Shift
EasyAI-assisted coding · AI-Assisted Debugging
Tip Pool: Fair Splits at the End of the Shift
You've joined the small team behind Pour House's back-of-house tools. At close, the shift lead runs the tip-pool script and hands everyone a payout sheet. Staff have been complaining that the numbers are off by a few dollars and that the manager "somehow always gets a cut." Three tickets are queued: fix the pool, add tip-outs and cash, and handle the new lunch/dinner split. All amounts are integer cents; there is no floating-point money anywhere.
Codebase
| File | What it does |
|---|---|
tips/api.py | run(operations) entry point and the Dispatcher that routes each operation |
tips/roster.py | Roster: staff ids and roles |
tips/timeclock.py | TimeCards: one clock-in/clock-out card per employee, in minutes |
tips/tipbook.py | TipBook: every tip (and later every sale) recorded during the night |
tips/allocation.py | split_cents: the one function that divides cents proportionally |
tips/pool.py | settle_night: turns the night into who-gets-what |
tips/report.py | Builds the payout sheet returned by settle |
tips/config.py | Role points, tip-out percentage, tip-eligible roles |
tips/errors.py | TipError and every error code |
tests/ | Existing unit tests (some of them currently fail) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh night and returns one result per operation. Errors are returned as "error: <code>" strings, never raised. Times are minutes since midnight (0-1440); a shift never crosses midnight.
| Operation | Arguments | Returns | Errors (checked in this order) |
|---|---|---|---|
staff | staff_id, role | true | invalid arguments, unknown role, duplicate staff |
clock | staff_id, start, end | true | invalid arguments, unknown staff, invalid hours, already clocked |
card_tip | at, staff_id, cents | true | invalid time, invalid amount, unknown staff, not tip eligible |
settle | none | the payout sheet (below) | no eligible staff |
- Roles are
server,bartender,busser,hostandmanager. Ids are non-empty, case-sensitive strings. clockneeds0 <= start < end <= 1440, otherwiseinvalid hours; each employee has at most one card. Non-integer times areinvalid arguments.atis the minute the tip was received and must be an integer in0..1439(invalid time). Cents must be an integer> 0(true/falseare not amounts). Only servers and bartenders can be tipped (not tip eligibleotherwise).- An unknown operation name returns
"error: unknown operation". A wrong number of arguments (or an empty/non-list operation) returns"error: invalid arguments". settlecan be called at any point, any number of times; it never changes the night.- Rows with the same weight compare ids as plain Python strings (
"e10" < "e2").
The payout sheet returned by settle:
{"pool": 15002, "tipout": 0,
"payouts": [{"id": "ana", "pool": 6697, "tipout": 0, "cash": 0, "total": 6697}, ...]}payouts has one row for every registered employee, managers included, sorted by id. total = pool + tipout + cash.
Part 1 - Fix the pool (TIPS-118, TIPS-131)
Goal: card tips are pooled and shared by points x minutes on the clock. Make settle do exactly that.
Complaints from the floor: "the numbers don't add up to what we collected", "I worked 3h50m and got paid like 3 hours", "the manager is in the pool".
Rules
- Pool = the sum of all card tips. Every employee's weight is
points x minutesof their card: server 10, bartender 8, busser 5, host 3, manager 0 (managers never share the pool, however long they are clocked in). - Employee
iis owedpool * weight_i / total_weightcents. Everybody gets the floor of that; the leftover cents (always fewer than the number of employees with a positive weight) are then handed out one cent each in order of largest fractional share (the remainder ofpool * weight_idivided bytotal_weight; compare as exact integers). Ties go to the smaller id. Payouts always add up to the pool exactly. - Employees with weight 0 (never clocked in, or manager) get 0.
- If the pool is above 0 and nobody has a positive weight,
settlereturns"error: no eligible staff". With a pool of 0 everyone gets 0 and there is no error. - Today the restaurant runs a single service period: in Part 1 and Part 2 examples every clock and tip is at minute 960 (16:00) or later.
Worked example
[["staff","ana","server"],["staff","ben","bartender"],["staff","cy","busser"],["staff","mo","manager"],
["clock","ana",1020,1320],["clock","ben",1080,1320],["clock","cy",1020,1380],["clock","mo",1020,1380],
["card_tip",1100,"ana",10000],["card_tip",1200,"ben",5002],["settle"]]Weights: ana 3000, ben 1920, cy 1800, mo 0. The pool is 15002 cents, so the floors are 6697, 4286 and 4018 (15001 in total). The one leftover cent goes to cy, whose fractional share (.39) is the largest. The sheet is pool 15002 with payouts ana 6697, ben 4286, cy 4019, mo 0.
Part 2 - Tip-outs and cash tips (TIPS-140)
Goal: servers tip out to the bussers, and cash tips are tracked. Two new operations:
| Operation | Arguments | Returns | Errors (in order) |
|---|---|---|---|
sales | at, staff_id, cents | true | invalid time, invalid amount, unknown staff, not tip eligible (only servers have sales) |
cash_tip | at, staff_id, cents, pooled | true | invalid arguments (if pooled is not a boolean), then as card_tip |
Rules
- A
cash_tipwithpooled = trueis added to the pool. Withpooled = falsethe server or bartender keeps it: it shows up in theircashcolumn and never touches the pool or a tip-out. - Each server tips out 2% of their sales to the bussers. First add up all of that server's
sales, then round once, half up to a whole cent ((sales * 2 + 50) // 100). - The tip-out is paid from the server's card tips: it can never exceed the server's total card tips (cash does not count), and what is paid is subtracted from what that server contributes to the pool.
- All servers' tip-outs are added into one pot that is split once among the bussers with
split_cents, weighted by minutes on the clock (not points). Each busser's share is theirtipoutcolumn. Bussers still take part in the main pool as in Part 1. - If no busser has a positive number of minutes, nothing is tipped out and nothing is subtracted.
poolin the sheet is the amount actually divided by points x minutes (card tips - tip-outs paid + pooled cash).tipoutis the size of the pot. Everything collected is paid out: the sum of all totals equals all card tips plus all cash tips.
Worked example
[["staff","ana","server"],["staff","ben","bartender"],["staff","cy","busser"],["staff","di","busser"],
["clock","ana",1020,1320],["clock","ben",1080,1320],["clock","cy",1020,1380],["clock","di",1200,1380],
["sales",1100,"ana",25225],["sales",1150,"ana",25],
["card_tip",1100,"ana",10000],["card_tip",1200,"ben",5002],
["cash_tip",1210,"ana",2000,true],["cash_tip",1220,"ben",1500,false],["settle"]]ana's sales total 25250, so her tip-out is 505 (rounding each sale separately would give 506). The pot of 505 is split 360:180 between cy and di: 337 and 168. The pool is 10000 - 505 + 5002 + 2000 = 16497. The sheet is pool 16497, tipout 505 and payouts ana 6495, ben 4157 (+1500 cash), cy 3897 (+337), di 1948 (+168).
Part 3 - Lunch and dinner pool separately (TIPS-152)
Goal: the owners now run lunch and dinner as two services. A shift that spans both must not let dinner's big tips subsidise someone's lunch hours (or the reverse).
Rules
- Lunch is minutes
0..959, dinner is960..1439. Minute 960 (16:00) is dinner. - Each period is settled on its own, using Parts 1 and 2 restricted to that period. A tip or a sale belongs to the period of its
at. An employee's weight in a period ispoints x minutes of their card inside that period(a card from 15:00 to 17:00 is 60 minutes of lunch and 60 minutes of dinner). - The largest-remainder split is done per period: each period's pool is split by itself, and so is each period's tip-out pot. Do not split the day's totals.
- The tip-out for a period uses only that period's sales, that period's card tips (for the cap) and the bussers' minutes inside that period. If no busser has minutes in a period, that period has no tip-out and no deduction.
- A period with pool above 0 and nobody with positive weight in it returns
"error: no eligible staff", even if the other period is fine. A period with a pool of 0 never errors. - The sheet adds the two periods together:
poolandtipoutare the day's totals, and each row is the sum of the employee's two periods. - When all clocks and tips fall in one period, Parts 1 and 2 behave exactly as before.
Worked example
[["staff","ana","server"],["staff","cy","busser"],["staff","di","host"],
["clock","ana",900,1080],["clock","cy",840,1020],["clock","di",930,1050],
["sales",800,"ana",10000],["sales",1000,"ana",20000],
["card_tip",850,"ana",3000],["card_tip",1000,"ana",7001],
["cash_tip",959,"ana",500,true],["cash_tip",960,"ana",900,true],["settle"]]Lunch: cy is the only busser so the 200 tip-out goes to cy, and the lunch pool is 3000 - 200 + 500 = 3300 split over weights ana 600, cy 600, di 90 (floors leave two stray cents; ana and cy tie on the largest remainder, so they get one each and di gets none). Dinner: tip-out 400 to cy, pool 7001 - 400 + 900 = 7501 over weights ana 1200, cy 300, di 270 (leftover cent to ana). The sheet is pool 10801, tipout 600 and payouts ana 6621, cy 2806 (+600), di 1374.
Example 1
- Input:
- [["staff","ana","server"],["staff","ben","bartender"],["staff","cy","busser"],["staff","mo","manager"],["clock","ana",1020,1320],["clock","ben",1080,1320],["clock","cy",1020,1380],["clock","mo",1020,1380],["card_tip",1100,"ana",10000],["card_tip",1200,"ben",5002],["settle"]]
- Output:
- {"pool":15002,"tipout":0,"payouts":[ana 6697, ben 4286, cy 4019, mo 0]}
- Explanation
- Weights are 3000, 1920, 1800 and 0 for the manager. Floors leave one cent over, which goes to cy, who has the largest fractional share.
Guidelines
- Python 3 standard library only
- Money is integer cents; no floating-point arithmetic anywhere in the split
- Times are integer minutes since midnight; a shift never crosses midnight
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- settle is a pure read: calling it repeatedly gives the same answer until new operations arrive