Bike Share: Fares, Full Stations and Day Passes
EasyAI-assisted coding · AI-Assisted Feature Work
Bike Share: Fares, Full Stations and Day Passes
You've joined the platform team behind a city bike-share app. Riders unlock a bike at one docking station and return it to another. Support is flooded with fare complaints and wrong "bikes available" numbers, and the product team has two changes queued behind them. You have three Parts, and each builds on the previous one. All money is integer cents and all times are integer seconds.
Codebase
| File | What it does |
|---|---|
bikeshare/api.py | run(operations) entry point and the Dispatcher that validates arguments and routes each operation |
bikeshare/fleet.py | Fleet: stations, bikes, who is docked where, repairs and station_status |
bikeshare/rides.py | RideService: unlock and dock, the open ride per rider, fare charging |
bikeshare/pricing.py | Pure fare rules (billed_minutes, ride_fare) |
bikeshare/riders.py | RiderBook: riders and the running amount billed to each |
bikeshare/passes.py | PassDesk: placeholder for Part 3 (buy_pass answers "not supported") |
bikeshare/models.py | Station, Bike, Rider, Ride dataclasses |
bikeshare/config.py | Tariff constants (fees, rates, free minutes, grace, pass price) |
bikeshare/errors.py | BikeShareError and every error code |
tests/ | Existing end-to-end tests (one of them currently fails) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh system and returns one result per operation. Every operation is [name, ts, ...args]. Errors are returned as "error: <code>" strings, never raised.
| Operation | Arguments after ts | Returns | Errors (checked in this order) |
|---|---|---|---|
add_station | station_id, capacity | true | station exists |
add_rider | rider_id, is_member | true | rider exists |
add_bike | bike_id, station_id, kind ("classic" or "ebike") | true (the bike starts docked) | bike exists, station not found, station full |
unlock | rider_id, bike_id | ride id "r-1", "r-2", ... | rider not found, bike not found, rider busy, bike unavailable |
dock | rider_id, station_id, broken | the ride's fare in cents | rider not found, no active ride, station not found |
repair | bike_id | true | bike not found, bike not broken |
station_status | station_id | {"capacity", "bikes", "available", "free"} | station not found |
rider_total | rider_id | cents billed to the rider so far | rider not found |
- A wrong number of arguments, a non-integer
ts, an empty or non-string id, acapacitythat is not an integer> 0, a non-boolean flag, or an unknown bike kind returns"error: invalid arguments". An unknown operation name returns"error: unknown operation". - Ride ids count up in the order unlocks succeed. A rider has at most one open ride.
- A bike is docked (it sits in a dock of exactly one station) or out on a ride.
unlocktakes the bike out of its dock.dockputs it into a dock at the given station;broken: truereports it as broken at return, and it stays broken untilrepair. A rider can unlock only a docked bike that is not broken (bike unavailableotherwise). A rider may return a bike to the station it came from. station_statusfields:capacity;bikes= bikes in docks (broken ones included);available= docked bikes that are not broken;free=capacity - bikes.dockandrider_totalcharge the fare to the rider.rider_totalis the sum of everything billed.
Tariff
- Unlock fee: 100c per ride; members pay none.
- Time charge: the ride lasts
dock_ts - unlock_tsseconds, and every *started* minute is billed: 0s is 0 minutes, 1s to 60s is 1 minute, 61s is 2 minutes. Each billed minute costs 15c (base rate). - Members ride the first 45 billed minutes of each ride at no base rate, then pay 15c for every minute beyond 45 (45 min = 0c, 46 min = 15c).
- E-bikes add a 25c surcharge for every billed minute, for members and non-members alike, including a member's free minutes.
Example: a non-member on a classic bike for 20 minutes pays 100 + 20x15 = 400. A member on an e-bike for 50 minutes pays 5x15 + 50x25 = 1325.
Part 1 - Fix the fare and availability bugs (about 10 min)
Support has three reports: "I was charged an extra minute on a ride of exactly one hour", "I'm a member and a 46-minute ride cost me the whole 46 minutes", and "the app said 3 bikes were available but two were broken". Make the system follow the rules above. tests/test_rides.py has one failing test to start from.
Worked example (stations A and B with 4 docks each; ann non-member, mo member; b1 classic and e1 e-bike docked at A, plus a second classic b2 at A that nobody touches):
["unlock", 100, "ann", "b1"] -> "r-1"
["dock", 1300, "ann", "B", false] -> 400
["unlock", 2000, "mo", "e1"] -> "r-2"
["dock", 5000, "mo", "B", false] -> 1325
["station_status", 5001, "B"] -> {"capacity": 4, "bikes": 2, "available": 2, "free": 2}
["unlock", 6000, "ann", "b1"] -> "r-3"
["dock", 6061, "ann", "A", true] -> 130 # 61s = 2 minutes: 100 + 30
["station_status", 6062, "A"] -> {"capacity": 4, "bikes": 2, "available": 1, "free": 2}Part 2 - Full stations and the grace period (about 15 min)
Today dock ignores capacity. Make a rider who arrives at a full station bounce off it without being penalised for the search.
dockat a station with no free dock returns"error: station full", checked afterstation not found. The ride stays open and the bike stays out; nothing else about the bike (including itsbrokenflag) or the station changes.- The first bounce of a ride records its
tsas that ride's *grace start*. Later bounces of the same ride do not move it. - When the ride finally docks successfully at
ts, the forgiven time ismin(ts - grace_start, 600)seconds. Subtract it fromts - unlock_tsbefore rounding up to billed minutes, then price as usual. - A ride that never bounced gets no grace. Grace belongs to one ride only.
- A bike reported broken at return still occupies its dock (it counts in
bikes, not inavailable), and a repaired bike keeps its dock.
Worked example (station F has capacity 1 and is already full of f1; ann is a non-member):
["unlock", 100, "ann", "b1"] -> "r-1"
["dock", 1000, "ann", "F", false] -> "error: station full" # grace starts at 1000
["dock", 1100, "ann", "F", false] -> "error: station full"
["dock", 1300, "ann", "A", false] -> 325 # 1200s - min(300, 600) = 900s = 15 min: 100 + 225Part 3 - Day passes (about 15 min)
Product wants a day pass for non-members: 1200c for unlimited rides within 24 hours, where each ride's first 30 minutes are free. This changes how a ride is priced, so rework the fare design rather than bolting a special case on.
buy_passcharges 1200c to the rider and returns1200. Errors in order:rider not found,already a member(members cannot buy passes),pass already held.- A pass is *held* from purchase until its window closes. It is not tied to the purchase time: the window starts at the rider's next successful
unlockat timeSand covers unlocks attswithS <= ts < S + 86400(half-open). A failedunlockdoes not start it. - A ride is pass-covered if its unlock happened inside the window (the unlock that starts the window is covered). This is decided at unlock and does not change if the ride ends after the window closes.
- A pass-covered ride pays no unlock fee and gets 30 free base-rate minutes (instead of the member's 45). Beyond them each billed minute costs 15c. The e-bike surcharge still applies to every billed minute. Billed minutes are computed as in Parts 1 and 2, including grace.
- Once the window has closed the pass is no longer held: unlocks are priced normally and the rider may buy another. A bought-but-unused pass counts as held.
Worked example (ann non-member, mo member, bikes as in Part 2):
["buy_pass", 10, "ann"] -> 1200
["buy_pass", 11, "ann"] -> "error: pass already held"
["buy_pass", 12, "mo"] -> "error: already a member"
["unlock", 100, "ann", "b1"] -> "r-1" # window is [100, 86500)
["dock", 2500, "ann", "A", false] -> 150 # 40 min: 10 paid minutes x 15
["unlock", 3000, "ann", "e1"] -> "r-2"
["dock", 4800, "ann", "A", false] -> 750 # 30 min free base, e-bike 30 x 25
["unlock", 86500, "ann", "b1"] -> "r-3" # window closed: normal pricing
["dock", 86560, "ann", "A", false] -> 115 # 1 min: 100 + 15
["rider_total", 86561, "ann"] -> 2215Hidden tests re-check every earlier Part after the later ones, so keep Parts 1 and 2 behaviour intact.
Example 1
- Input:
- [...2 stations, ann (non-member), mo (member), b1 classic, e1 ebike at A..., ["unlock",100,"ann","b1"],["dock",1300,"ann","B",false],["unlock",2000,"mo","e1"],["dock",5000,"mo","B",false]]
- Output:
- [...,"r-1",400,"r-2",1325]
- Explanation
- Ann rides 20 minutes: 100c unlock + 20 x 15c = 400. Mo (member) rides an e-bike for 50 minutes: 5 minutes beyond the free 45 at 15c = 75, plus 50 x 25c e-bike surcharge = 1250, total 1325.
Example 2
- Input:
- [...dock F has capacity 1 and is full..., ["unlock",100,"ann","b1"],["dock",1000,"ann","F",false],["dock",1100,"ann","F",false],["dock",1300,"ann","A",false]]
- Output:
- [...,"r-1","error: station full","error: station full",325]
- Explanation
- The first bounce at 1000 starts the grace clock. Docking at 1300 forgives min(300, 600) = 300 seconds, so 900 seconds = 15 minutes: 100 + 225 = 325.
Example 3
- Input:
- [...,["buy_pass",10,"ann"],["unlock",100,"ann","b1"],["dock",2500,"ann","A",false]]
- Output:
- [...,1200,"r-1",150]
- Explanation
- The pass is bought for 1200c and the window starts at the unlock (100). A 40-minute ride has 30 free minutes and 10 paid minutes at 15c, with no unlock fee.
Guidelines
- Python 3 standard library only
- All money is integer cents; all timestamps and durations are integer seconds
- Timestamps never decrease from one operation to the next
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised