Community Garden: Plot Waitlist, Season Fees and Swaps
EasyAI-assisted coding · AI-Assisted Debugging
Community Garden: Plot Waitlist, Season Fees and Swaps
You've joined the volunteer-run tools team for the city's community-garden program. Gardeners apply for a plot, wait in line, and are handed a plot when one frees up. The coordinators report that the wrong people are getting plots, and three changes are queued behind that: season fees, plot swaps, and (from the board) a one-plot-per-household rule. Money is integer cents.
Codebase
| File | What it does |
|---|---|
garden/api.py | run(operations) entry point and the Dispatcher that routes each operation |
garden/garden.py | Garden: adding plots, applying, offering and releasing plots, holdings |
garden/waitlist.py | The waiting list: priority class, priority order, first eligible applicant |
garden/eligibility.py | Which applicant may hold which plot |
garden/plots.py | PlotRegistry: plots and who holds each one |
garden/fees.py | Season fees (Part 2) |
garden/swaps.py | Plot swaps (Part 2), currently empty |
garden/household.py | Household identity (Part 3), currently a stub |
garden/config.py | Sizes, season months, fee tariff |
garden/dates.py, models.py, errors.py | Date parsing, data classes, every error code |
tests/ | Existing tests (some currently fail) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh garden and returns one result per operation. Every operation is [name, date, ...args], where date is an ISO "YYYY-MM-DD" string for the day the operation happens. Dates are not required to be in order (paper applications are keyed in late). Errors are returned as "error: <code>" strings, never raised.
| Operation | Arguments after date | Returns |
|---|---|---|
add_plot | plot_id, size, raised | true; the plot starts vacant |
apply | applicant_id, resident, accessibility, size_pref, address | true; the applicant joins the waitlist with date as their application date |
withdraw | applicant_id | true; removes a waiting applicant |
offer | plot_id | id of the applicant who received the vacant plot, or null if nobody is eligible |
release | plot_id | the gardener gives the plot up and it is offered at once; returns the new holder's id or null |
holder | plot_id | current holder's id or null |
waitlist | none | ids of everyone waiting, in priority order |
sizeis"small"or"large";raisedis a boolean (a raised-bed plot).size_prefis"small","large"or"either";residentandaccessibilityare booleans.- Error order for every operation:
unknown operation, theninvalid arguments(wrong argument count or type, badsize/size_pref), theninvalid date, then the operation's own errors. - Own errors:
add_plot→plot exists.apply→applicant exists(the id is already waiting or already holds a plot).withdraw→applicant not found(not waiting).offer→plot not found,plot occupied.release→plot not found,plot vacant.holder→plot not found. - A gardener who released a plot is no longer a gardener and may apply again.
- A vacant plot nobody could take stays vacant until a later
offerfor it.applynever assigns a plot by itself.
Part 1: Fix who gets the plot
Goal: when a plot is offered, it must go to the right applicant. Coordinators have found wrong winners in three different situations. Find and fix the causes; do not change the operation vocabulary.
Rules
- Priority order of the waitlist, front first: (a) applicants with an accessibility need, whether or not they live in the city; (b) residents; (c) non-residents. Within each group the earlier application date goes first; the same date goes by arrival order, meaning the order in which the
applyoperations were processed. Applicant ids are never used to order people. - A raised-bed plot may only go to an applicant with an accessibility need. Any other plot is open to everyone whose size preference accepts it:
"small"accepts only small plots,"large"only large plots,"either"accepts both. - An offer walks the waitlist from the front and gives the plot to the first eligible applicant.
"either"earns no priority and no penalty: an"either"applicant is simply eligible for more plots. Applicants who are skipped keep their place. - The winner leaves the waitlist and holds the plot.
Example
run([
["add_plot", "2025-03-01", "R1", "small", True],
["add_plot", "2025-03-01", "L1", "large", False],
["apply", "2025-01-10", "ines", True, False, "large", "4 Elm St"],
["apply", "2025-01-12", "pat", False, True, "either", "9 Oak St"],
["apply", "2025-01-05", "sam", True, False, "either", "7 Pine Rd"],
["waitlist", "2025-03-01"],
["offer", "2025-03-02", "R1"],
["offer", "2025-03-02", "L1"],
["waitlist", "2025-03-02"],
])returns [true, true, true, true, true, ["pat", "sam", "ines"], "pat", "sam", ["ines"]]. Pat has an accessibility need, so leads the line and is the only one eligible for the raised bed. Sam applied before Ines and accepts either size, so Sam wins the large plot.
Part 2: Season fees and plot swaps
Goal: a plot is paid for when it is assigned, prorated for the part of the season left. Add ["invoice", date, applicant_id] and ["swap", date, first_id, second_id].
Rules
- The season runs April through October (7 months) of the calendar year of the assignment date. The assignment date is the
dateof theofferorreleasethat assigned the plot. - Remaining months are whole calendar months: before April 1 → 7; after October 31 → 0; otherwise the months after the assignment month through October, plus the assignment month itself only when the assignment is on the 1st. April 1 → 7, June 1 → 5, June 2 → 4, October 1 → 1, October 2 → 0.
fee = base × percent × remaining months ÷ (100 × 7), computed on the whole product, rounded half up to a whole cent once, at the end. Base: small 6125, large 10500. Percent by priority class: accessibility need 50, resident 100, non-resident 150 (class as inwaitlist.py).- The fee is fixed when the plot is assigned and stays with the gardener; it is never recomputed.
invoicereturns{"plot": plot_id, "months": n, "fee": cents}for a current gardener, or"error: not a gardener".swapexchanges the plots of two current gardeners if each is eligible for the other's plot (the same eligibility rules as an offer). It returnstrue. Errors, in order:invalid arguments(ids not strings),same gardener(both ids equal),not a gardener(first id, then second id),not eligible. A failed swap changes nothing. Fees, months and assignment dates do not move.
Example
run([
["add_plot", "2025-03-01", "S1", "small", False],
["add_plot", "2025-03-01", "L1", "large", False],
["apply", "2025-02-01", "ana", True, True, "either", "1 A St"],
["apply", "2025-02-02", "bo", True, False, "either", "2 B St"],
["offer", "2025-07-15", "S1"],
["offer", "2025-07-16", "L1"],
["invoice", "2025-07-20", "ana"],
["invoice", "2025-07-20", "bo"],
["swap", "2025-07-21", "ana", "bo"],
["holder", "2025-07-21", "S1"],
["invoice", "2025-07-21", "ana"],
])returns [true, true, true, true, "ana", "bo", {"plot": "S1", "months": 3, "fee": 1313}, {"plot": "L1", "months": 3, "fee": 4500}, true, "bo", {"plot": "L1", "months": 3, "fee": 1313}]. Ana's fee is 6125 × 50 × 3 ÷ 700 = 1312.5, which rounds up to 1313, and it does not change when she moves to the large plot.
Part 3: One plot per household
Goal: the board has decided that a household may hold at most one plot at a time, and the rule applies to people already waiting. Add ["household", date, address] and enforce the cap.
Rules
- A household is identified by its normalized address: lowercase; commas and periods become spaces;
#becomes the wordapt; split on whitespace, then replace the wordsstreetavenueroaddriveapartmentwithstaverddrapt; a run of repeatedaptwords becomes one; join with single spaces."12 Oak Street #4"and"12 OAK ST., Apt 4"are both"12 oak st apt 4". Different unit numbers are different households. - An offer gives the plot to the first applicant, in the Part 1 order, who is eligible and whose household holds no plot at that moment. Skipped applicants stay on the waitlist in place. Nobody is removed from the waitlist because of the cap, and applying is never refused because of it.
- The check uses plots held right now. In
release, the releasing gardener's plot is vacated before the plot is offered, so someone from the same household may immediately win it back. applywith an address that normalizes to the empty string returns"error: invalid address"(checked afterinvalid argumentsand beforeapplicant exists).["household", date, address]returns the ids of the current gardeners in that household, sorted ascending (plain string order). Non-string address →invalid arguments; address that normalizes to empty →invalid address.- Swaps are unaffected by the cap; Parts 1 and 2 keep working as before.
Example
run([
["add_plot", "2025-03-01", "A", "small", False],
["add_plot", "2025-03-01", "B", "small", False],
["apply", "2025-01-01", "kim", True, False, "small", "12 Oak Street #4"],
["apply", "2025-01-02", "lee", True, False, "small", "12 OAK ST., Apt 4"],
["apply", "2025-01-03", "max", True, False, "small", "3 Elm Rd"],
["offer", "2025-03-02", "A"],
["offer", "2025-03-02", "B"],
["waitlist", "2025-03-02"],
["household", "2025-03-02", "12 oak st apt 4"],
["release", "2025-03-03", "A"],
["waitlist", "2025-03-03"],
])returns [true, true, true, true, true, "kim", "max", ["lee"], ["kim"], "lee", []]. Lee is skipped because Kim's household holds plot A, so Max gets B. When Kim releases A, Kim's household holds nothing again and Lee is first in line.
Notes
plots.py,dates.pyandmodels.pyare known-good.- You may use the AI assistant freely; you are expected to verify what it produces.
- Hidden tests re-check earlier Parts after later ones.
Example 1
- Input:
- [["add_plot","2025-03-01","R1","small",true],["add_plot","2025-03-01","L1","large",false],["apply","2025-01-10","ines",true,false,"large","4 Elm St"],["apply","2025-01-12","pat",false,true,"either","9 Oak St"],["apply","2025-01-05","sam",true,false,"either","7 Pine Rd"],["waitlist","2025-03-01"],["offer","2025-03-02","R1"],["offer","2025-03-02","L1"],["waitlist","2025-03-02"]]
- Output:
- [true,true,true,true,true,["pat","sam","ines"],"pat","sam",["ines"]]
- Explanation
- Pat has an accessibility need so leads the line and is the only one eligible for the raised bed. Sam applied before Ines and accepts either size, so Sam wins the large plot.
Example 2
- Input:
- [["add_plot","2025-03-01","S1","small",false],["add_plot","2025-03-01","L1","large",false],["apply","2025-02-01","ana",true,true,"either","1 A St"],["apply","2025-02-02","bo",true,false,"either","2 B St"],["offer","2025-07-15","S1"],["offer","2025-07-16","L1"],["invoice","2025-07-20","ana"],["swap","2025-07-21","ana","bo"],["invoice","2025-07-21","ana"]]
- Output:
- [true,true,true,true,"ana","bo",{"plot":"S1","months":3,"fee":1313},true,{"plot":"L1","months":3,"fee":1313}]
- Explanation
- Assigned on July 15, three whole months (Aug, Sep, Oct) remain. Ana pays 6125 x 50% x 3/7 = 1312.5, rounded half up to 1313, and the fee stays with her after the swap.
Example 3
- Input:
- [["add_plot","2025-03-01","A","small",false],["add_plot","2025-03-01","B","small",false],["apply","2025-01-01","kim",true,false,"small","12 Oak Street #4"],["apply","2025-01-02","lee",true,false,"small","12 OAK ST., Apt 4"],["apply","2025-01-03","max",true,false,"small","3 Elm Rd"],["offer","2025-03-02","A"],["offer","2025-03-02","B"],["release","2025-03-03","A"]]
- Output:
- [true,true,true,true,true,"kim","max","lee"]
- Explanation
- Lee shares Kim's normalized address, so is skipped for plot B and Max wins it. After Kim releases A, Lee's household holds nothing and Lee gets the plot.
Guidelines
- Python 3 standard library only
- Fees are integer cents; there is no floating-point money anywhere
- Dates are ISO YYYY-MM-DD strings and are not guaranteed to arrive in order
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised