Tool Library: Deposits, Tiers and Suspensions
EasyAI-assisted coding · AI-Assisted Debugging
Maple Street Tool Library: Deposits, Tiers and Suspensions
You've joined the volunteer team that runs the checkout software for the Maple Street Tool Library, a neighborhood lending shelf. Members borrow tools for a week against a refundable deposit. The desk volunteers have filed three complaints about the current build, and the board has two policy changes queued behind them. All money is integer cents (no floating-point money anywhere) and all dates are YYYY-MM-DD strings.
You have three Parts. Later Parts must not break earlier ones: hidden tests re-check earlier behavior after the later features are in.
Codebase
| File | What it does |
|---|---|
toollib/api.py | run(operations) entry point and the Dispatcher that routes each operation |
toollib/service.py | ToolLibraryService: checkout, return, inspection, member and loan reports |
toollib/loans.py | LoanBook (every loan, plus the limit and availability lookups) and overdue_days |
toollib/penalties.py | PenaltyTracker: late-return bookkeeping and suspensions (currently a stub) |
toollib/deposits.py | Deposit arithmetic |
toollib/catalog.py | Catalog: the tools the library owns |
toollib/members.py | MemberRegistry: joining and lookup |
toollib/models.py | Tool, Member, Loan records and the loan-state constants |
toollib/dates.py | Date parsing, formatting and day arithmetic |
toollib/config.py | Handbook policy constants (tiers, loan length, deposit percent, suspension numbers) |
toollib/errors.py | Every error code |
tests/ | Existing tests (one of them currently fails) |
How your code is run
run(operations: list[list]) -> list runs the operations in order against a fresh, empty library and returns one result per operation. Every operation is [name, date, ...args]. Dates never go backwards from one operation to the next. A malformed date (not exactly YYYY-MM-DD, or not a real calendar day) returns "error: invalid date". Errors are returned as "error: <code>" strings, never raised. An unknown operation returns "error: unknown operation"; a wrong number of arguments returns "error: invalid arguments".
| Operation | Arguments after the date | Returns | Errors (checked in this order) |
|---|---|---|---|
add_tool | tool_id, name, value_cents | true | invalid arguments (empty id or name), invalid value, tool exists |
join | member_id, tier | true | invalid arguments (empty id), unknown tier, member exists |
checkout | member_id, tool_id | {"loan", "due", "deposit"} | member not found, tool not found, member suspended, tool unavailable, limit reached |
return | loan_ref, condition | {"refund", "held", "overdue_days", "suspended_until"} | invalid condition, loan not found, already returned |
loan | loan_ref | {"member", "due", "lines": [{"tool", "deposit", "state"}]} | loan not found |
member | member_id | {"tier", "active", "overdue_days", "late_returns", "suspended_until", "deposits_held"} | member not found |
Part 1: Fix the front desk
Goal: make checkout and return follow the handbook. The volunteers report that deposits are sometimes a few cents off, that members who bring everything back are told they are at their limit, and that tools returned on the due date are flagged late. Find the causes in the code (do not patch the API layer) and fix them.
Rules
value_centsmust be an integer> 0(true/falseare not values). Tool ids and member ids are case-sensitive strings.- Deposit = 15% of the tool's
value_cents, rounded up to a whole dollar, in cents. A tool worth4999has a deposit of800(15% is $7.4985); a tool worth8000has exactly1200. - Tiers:
basicmembers may have 2 items out at once,plusmembers 5. A loan stops counting against the limit as soon as its tool is returned. A checkout that would exceed the limit returnslimit reached. - A loan is due 7 days after the checkout date (
2026-03-01is due2026-03-08).dueis reported as a date string. Loan ids are"L-1","L-2", ... in the order checkouts succeed; failed checkouts do not use up an id. returntakes the condition"ok"(anything else isinvalid conditionfor now). The whole deposit is refunded:refund= the deposit,held= 0.overdue_days= whole days after the due date (returning on the due date is on time: 0). A returned tool is available again at once.suspended_untilis alwaysnullin this Part.memberreportsactive(items currently counting against the limit) anddeposits_held(deposits of tools still out).overdue_days,late_returnsandsuspended_untilare0,[]andnulluntil Part 2 maintains them.loanlists one line per tool on the loan, with the tool's deposit and astate("out"or"returned"for now).
Example
[["add_tool", "2026-03-01", "drill", "Cordless drill", 8000],
["add_tool", "2026-03-01", "saw", "Hand saw", 4999],
["join", "2026-03-01", "ana", "basic"],
["checkout", "2026-03-01", "ana", "drill"],
["checkout", "2026-03-02", "ana", "saw"],
["return", "2026-03-08", "L-1", "ok"],
["member", "2026-03-08", "ana"]]returns
[true, true, true,
{"loan": "L-1", "due": "2026-03-08", "deposit": 1200},
{"loan": "L-2", "due": "2026-03-09", "deposit": 800},
{"refund": 1200, "held": 0, "overdue_days": 0, "suspended_until": null},
{"tier": "basic", "active": 1, "overdue_days": 0, "late_returns": [], "suspended_until": null, "deposits_held": 800}]Part 2: Late returns, suspensions and damaged tools
Goal: the board wants chronic late borrowers paused and damaged tools handled properly. Maintain the member's lateness record, suspend borrowing, and add the damage-inspection flow.
Rules
- A return is late when
overdue_days > 0. Each late return adds itsoverdue_daysto the member's runningoverdue_days, and appends the return date tolate_returns(oldest first). On-time returns change neither. - Suspension: when a late return makes the member have
3or more late returns in a 90-day window, borrowing is suspended for 14 days. The window is the 90 calendar days ending on (and including) the date of the late return that was just recorded: an earlier late return counts if it is at most 89 days before it. Late returns on the same day all count. The member may check out again onreturn date + 14 days(suspended through the day before it). - If the member is already suspended when another qualifying late return happens, the suspension end becomes the later of the existing end and
return date + 14 days; it never shortens. - A suspended member's
checkoutreturnsmember suspended(see the table for where this check sits). Thereturnresult'ssuspended_until, andmember'ssuspended_until, show the date borrowing resumes if the member is still suspended on that operation's date, otherwisenull. returnalso accepts the condition"damaged":refundis 0 andheldis the tool's whole deposit; the loan's state becomes"inspection". The tool is not available again, but the loan stops counting against the tier limit. A damaged return is still checked for lateness (rules 1 to 3).deposits_heldincludes deposits held for inspection.- New operation
inspect[date, loan_ref, verdict]with verdict"ok"or"charge <cents>"(for example"charge 1500", digits only): returns{"refund", "billed"}and sets the state to"settled", making the tool available again."ok"refunds the whole deposit."charge X"refundsmax(0, deposit - X)and billsmax(0, X - deposit). Errors in order:invalid verdict,loan not found,not awaiting inspection(the loan was not returned damaged, or was already inspected).
Example (ana is a plus member; three tools t1, t2, t3 each worth 8000, all checked out on 2026-03-01 so they are due 2026-03-08)
[["return", "2026-03-10", "L-1", "ok"],
["return", "2026-03-10", "L-2", "ok"],
["return", "2026-03-10", "L-3", "damaged"],
["checkout", "2026-03-23", "ana", "t1"],
["checkout", "2026-03-24", "ana", "t1"],
["inspect", "2026-03-24", "L-3", "charge 1500"]]returns
[{"refund": 1200, "held": 0, "overdue_days": 2, "suspended_until": null},
{"refund": 1200, "held": 0, "overdue_days": 2, "suspended_until": null},
{"refund": 0, "held": 1200, "overdue_days": 2, "suspended_until": "2026-03-24"},
"error: member suspended",
{"loan": "L-4", "due": "2026-03-31", "deposit": 1200},
{"refund": 0, "billed": 300}]Part 3: Deck kits (policy change)
Goal: the library is adding bundles, such as a "deck kit" of three tools, that are borrowed as one loan. The board's decision changes how some earlier rules apply. Bundle support has to fit into the existing flow without changing any behavior for single tools.
New operations: add_bundle [date, bundle_id, [tool_id, ...]] returns true (errors in order: invalid arguments when the id is empty or the list has fewer than 2 tools or repeats a tool, bundle exists, tool not found for the first unknown tool in list order) and checkout_bundle [date, member_id, bundle_id] (errors in order: member not found, bundle not found, member suspended, tool unavailable if any of its tools is out or awaiting inspection, limit reached). Bundle ids are a separate namespace from tool ids.
Rules
- A bundle loan is one loan with one line per tool, in bundle order. It uses a single slot toward the tier limit, and keeps using it while any of its tools is still out. Tools awaiting inspection do not count. The
checkout_bundleresult is{"loan", "due", "deposit", "deposits"}:depositis the total anddepositsmaps each tool id to its own deposit, computed per tool exactly as in Part 1 (rounded up per tool, then added: three tools worth1000each have deposits of200, total600). - Tools are returned one at a time. A line is addressed by a loan reference:
"L-4/saw"(loan id, a slash, the tool id).returnandinspectaccept references of this form. For a single-tool loan the plain"L-1"still works (and"L-1/<its tool>"too); a reference to a tool that is not on the loan isloan not found. A plain loan id for a bundle loan istool required. The checks run in this order:invalid condition(orinvalid verdict),loan not found,tool required,already returned(ornot awaiting inspection). - Each returned tool is refunded, or held, or inspected separately using its own deposit.
damagedholds only that tool's deposit. - Every tool on a bundle loan is due at the same date, and every tool returned late adds its own
overdue_daysto the member's running total. But a bundle loan produces at most one late return for the 3-in-90 rule: only the first late tool returned adds an entry (its date) tolate_returns; later late tools of the same loan do not. (A late return of a single-tool loan is, as before, one entry.) - The
returnresult for a bundle line has the same shape as before;suspended_untilfollows the same rules.loanreports every line with its own state.member.activecounts slots as in rule 1.
Example (ana is plus; saw 4999, drill 8000, sander 2500; bundle deck-kit is [saw, drill, sander])
[["checkout_bundle", "2026-03-01", "ana", "deck-kit"],
["return", "2026-03-08", "L-1/saw", "ok"],
["return", "2026-03-08", "L-1", "ok"],
["return", "2026-03-11", "L-1/drill", "damaged"],
["return", "2026-03-11", "L-1/sander", "ok"],
["member", "2026-03-11", "ana"]]returns
[{"loan": "L-1", "due": "2026-03-08", "deposit": 2400, "deposits": {"saw": 800, "drill": 1200, "sander": 400}},
{"refund": 800, "held": 0, "overdue_days": 0, "suspended_until": null},
"error: tool required",
{"refund": 0, "held": 1200, "overdue_days": 3, "suspended_until": null},
{"refund": 400, "held": 0, "overdue_days": 3, "suspended_until": null},
{"tier": "plus", "active": 0, "overdue_days": 6, "late_returns": ["2026-03-11"],
"suspended_until": null, "deposits_held": 1200}]Notes
config.py,members.pyanddates.pyare known-good.- You may use the AI assistant freely; you are expected to verify what it produces.
- Existing tests in
tests/are a starting point only; hidden tests cover the boundaries above.
Example 1
- Input:
- [["add_tool","2026-03-01","drill","Cordless drill",8000],["add_tool","2026-03-01","saw","Hand saw",4999],["join","2026-03-01","ana","basic"],["checkout","2026-03-01","ana","drill"],["checkout","2026-03-02","ana","saw"],["return","2026-03-08","L-1","ok"]]
- Output:
- [true,true,true,{"loan":"L-1","due":"2026-03-08","deposit":1200},{"loan":"L-2","due":"2026-03-09","deposit":800},{"refund":1200,"held":0,"overdue_days":0,"suspended_until":null}]
- Explanation
- The drill's deposit is exactly 15% ($12.00); the saw's 15% is $7.4985, rounded up to $8.00. Returning on the due date is on time.
Example 2
- Input:
- [... three loans L-1..L-3 due 2026-03-08 ..., ["return","2026-03-10","L-1","ok"],["return","2026-03-10","L-2","ok"],["return","2026-03-10","L-3","damaged"],["checkout","2026-03-23","ana","t1"]]
- Output:
- [..., {"refund":1200,"held":0,"overdue_days":2,"suspended_until":null}, {"refund":1200,"held":0,"overdue_days":2,"suspended_until":null}, {"refund":0,"held":1200,"overdue_days":2,"suspended_until":"2026-03-24"}, "error: member suspended"]
- Explanation
- The third late return inside 90 days suspends Ana until 2026-03-24; the damaged tool's deposit is held for inspection.
Guidelines
- Python 3 standard library only
- Money is integer cents; there is no floating-point money anywhere
- Dates are YYYY-MM-DD strings and never decrease from one operation to the next
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Later Parts must not change behavior for single-tool loans