Studio Booking: Class Packs, Waitlists and Cancellations
EasyAI-assisted coding · AI-Assisted Debugging
Studio Booking: Class Packs, Waitlists and Cancellations
You've joined the team behind the booking backend of a boutique fitness studio. Members buy class packs (a number of credits that expire), spend one credit per class, and join a waitlist when a class is full. Front desk has reported three money-and-seat bugs, and two features are queued behind them. All times are integer minutes; a day is 1440 minutes.
Codebase
| File | What it does |
|---|---|
studio/api.py | run(operations) entry point and the Studio object that routes each operation |
studio/booking.py | BookingService: book, cancel, roster, credits |
studio/packs.py | PackLedger: buying packs, spending a credit, refunding a credit |
studio/schedule.py | ClassSchedule: the catalogue of classes |
studio/rules.py | Time-window helpers shared by operations (not started yet, late cancel) |
studio/models.py | Pack, Booking, GymClass dataclasses |
studio/config.py | Window sizes: LATE_CANCEL_MINUTES, PROMOTION_CUTOFF_MINUTES, pack id prefix |
studio/errors.py | StudioError and every error code |
tests/test_booking.py | Example tests (one of them currently fails) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh studio and returns one result per operation. Every operation is [name, ts, ...args]; ts is an integer number of minutes and never decreases. Errors are returned as "error: <code>" strings, never raised.
| Operation | Arguments after ts | Returns | Errors (checked in this order) |
|---|---|---|---|
add_class | class_id, start, capacity | true | class exists, invalid class |
buy_pack | member, credits, valid_days | pack id, e.g. "p-1" | invalid pack |
book | member, class_id | "booked" or "waitlisted:<position>" | class not found, class started, already booked, already waitlisted, no credits |
cancel | member, class_id | "refunded", "forfeited" or "left waitlist" | class not found, class started, not registered |
roster | class_id | {"booked": [...], "waitlist": [...]} | class not found |
credits | member | usable credits (int) | none |
- A name that is not in the table returns
"error: unknown operation". A wrong number of arguments, a non-integer (or boolean) where an integer is expected, a non-string or empty id, or a non-integertsreturns"error: invalid arguments". add_class:invalid classwhenstart <= tsorcapacity < 1.buy_pack:invalid packwhencredits < 1orvalid_days < 1.- Packs. Pack ids are
"p-1","p-2", ... in purchase order across the whole studio. A pack bought attsexpires atts + valid_days * 1440; it is usable at timetonly whilet < expires.creditscounts the credits left in the member's packs that are usable atts. - Spending. Booking a seat spends one credit from the member's usable pack that expires soonest (ties: lower pack id). A booking remembers which pack paid for it.
- Booking a full class puts the member on the waitlist (position 1 = first in line) and costs nothing; the credit is only spent if they are later promoted. A class is full when
booked == capacity. - A booking, waitlist join or cancel needs
ts < start; atts >= startthe answer isclass started. - Cancelling. A waitlisted member leaves the waitlist (
left waitlist, no credit involved). A booked member frees the seat and the cancel is late if the class starts in less than 720 minutes (12 hours); exactly 720 is still on time. A late cancel returnsforfeitedand the credit is gone. An on-time cancel returnsrefundedand the credit goes back to the pack that paid for that seat, even if that pack has expired in the meantime (an expired pack cannot spend it, so the credit is simply lost). rosterlists booked members in the order seats were taken and the waitlist in line order.
Worked example
[["add_class", 0, "yoga", 3000, 2],
["buy_pack", 1, "ana", 5, 30],
["book", 2, "ana", "yoga"],
["credits", 3, "ana"],
["cancel", 100, "ana", "yoga"],
["credits", 101, "ana"]]returns
[true, "p-1", "booked", 4, "refunded", 5]Part 1: Fix three booking bugs (STU-412, STU-419, STU-427)
Goal: make booking, cancelling and refunding follow the rules above.
Reports from the front desk:
- "Ana cancelled exactly 12 hours before class and lost her credit."
- "Ben bought a 2-credit pack that expires this week and a 20-credit pack that expires in two months. The system spent from the big pack first and his small pack expired unused."
- "Cy cancelled in time and got his credit back, but into the wrong pack: his short pack still expired with a credit in it, and his long pack grew by one."
tests/test_booking.py::test_cancel_exactly_twelve_hours_before_is_refunded currently fails. Waitlists still never get promoted; that is Part 2.
Rules: everything in the section above; the table's error order holds.
Part 2: Waitlist auto-promotion (STU-431)
Goal: when a booked member cancels, offer the freed seat to the waitlist.
Rules
- After a cancel of a booked seat (refunded or forfeited alike), if the class starts more than 120 minutes after
ts(PROMOTION_CUTOFF_MINUTES; exactly 120 is too late), try to promote one waitlisted member. At 120 minutes or less, nobody is promoted and the seat stays open for walk-ins. - Go through the waitlist in line order. A member is promoted if they have a usable credit at
ts: one credit is spent exactly as forbook(soonest-expiring usable pack), they get a seat appended to the bookings, and they leave the waitlist. Members with no usable credit are skipped and keep their place. At most one member is promoted per cancel. Cancelling a waitlist spot never promotes anyone. - An open seat with a non-empty waitlist (rule 1 cutoff, or everyone was skipped) can be taken by a
bookfrom anyone not already booked or waitlisted: it books directly ("booked"). A waitlisted member callingbookstill getsalready waitlisted. - A promoted member's booking remembers the pack that paid at promotion time; their own later cancel follows the Part 1 rules against that pack.
Example
[["add_class", 0, "spin", 3000, 1],
["buy_pack", 1, "ana", 5, 30], ["buy_pack", 1, "ben", 5, 30], ["buy_pack", 1, "cy", 5, 30],
["book", 2, "ana", "spin"], ["book", 3, "ben", "spin"], ["book", 4, "cy", "spin"],
["cancel", 100, "ana", "spin"],
["roster", 101, "spin"], ["credits", 101, "ben"], ["credits", 101, "ana"]]returns [true, "p-1", "p-2", "p-3", "booked", "waitlisted:1", "waitlisted:2", "refunded", {"booked": ["ben"], "waitlist": ["cy"]}, 4, 5].
Part 3: Instructor-cancelled classes (STU-448)
Goal: the studio can cancel a whole class. Everyone who booked must be made whole, and this changes how refunds to expired packs work for that case.
New operation: ["cancel_class", ts, class_id] returns {"refunded": <int>, "makeup_packs": <int>, "cleared": <int>}. Errors, in order: class not found, class cancelled, class started (ts >= start).
Rules
- Every booked member gets their credit back regardless of how close the class is; the late-cancel window does not apply. Bookings are processed in booking order. If the pack that paid is still usable at
tsthe credit goes back to it. If it has expired, the credit instead goes into a new makeup pack: 1 credit, valid for 14 days (ts + 14 * 1440), with the next pack id ("p-N"continues the same sequence, so ids follow booking order). - The waitlist is cleared; waitlisted members were never charged, so nothing is refunded to them. Nobody is promoted.
- Members who cancelled earlier (including late cancels) get nothing from
cancel_class. The Part 1 rule for a member's own cancel is unchanged: their credit returns to the original pack even if it expired. refunded= number of bookings refunded,makeup_packs= how many of them got a makeup pack,cleared= waitlist size removed.- Afterwards the class stays in the catalogue:
rosterreturns two empty lists, andbookandcancelreturnclass cancelled(checked right afterclass not found, beforeclass started, even after the original start time).add_classwith the same id is stillclass exists.
Example (ana, ben, cy each own a fresh 5-credit pack)
[["add_class", 0, "pilates", 3000, 2],
["buy_pack", 1, "ana", 5, 30], ["buy_pack", 1, "ben", 5, 30], ["buy_pack", 1, "cy", 5, 30],
["book", 2, "ana", "pilates"], ["book", 3, "ben", "pilates"], ["book", 4, "cy", "pilates"],
["cancel_class", 2900, "pilates"],
["credits", 2901, "ana"], ["credits", 2901, "cy"],
["roster", 2902, "pilates"], ["book", 2903, "cy", "pilates"]]returns [true, "p-1", "p-2", "p-3", "booked", "booked", "waitlisted:1", {"refunded": 2, "makeup_packs": 0, "cleared": 1}, 5, 5, {"booked": [], "waitlist": []}, "error: class cancelled"].
Notes
- You may use the AI assistant freely; you are expected to verify what it produces.
- Later parts must not break earlier ones; hidden tests re-check earlier behaviour.
schedule.pyandmodels.pyare known-good.
Example 1
- Input:
- [["add_class",0,"yoga",3000,2],["buy_pack",1,"ana",5,30],["book",2,"ana","yoga"],["credits",3,"ana"],["cancel",100,"ana","yoga"],["credits",101,"ana"]]
- Output:
- [true,"p-1","booked",4,"refunded",5]
- Explanation
- Ana books and spends one of her five credits. She cancels with plenty of notice, so the credit returns to the pack that paid.
Example 2
- Input:
- [["add_class",0,"spin",3000,1],["buy_pack",1,"ana",5,30],["buy_pack",1,"ben",5,30],["buy_pack",1,"cy",5,30],["book",2,"ana","spin"],["book",3,"ben","spin"],["book",4,"cy","spin"],["cancel",100,"ana","spin"],["roster",101,"spin"]]
- Output:
- [true,"p-1","p-2","p-3","booked","waitlisted:1","waitlisted:2","refunded",{"booked":["ben"],"waitlist":["cy"]}]
- Explanation
- The class has one seat, so Ben and Cy wait. When Ana cancels with the class far away, Ben is promoted and pays a credit.
Guidelines
- Python 3 standard library only
- All times are integer minutes; a day is 1440 minutes; ts never decreases from one operation to the next
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised