Toll Road: Trip Billing
EasyAI-assisted coding · AI-Assisted Debugging
Toll Road: Trip Billing
You've joined the tolling team of a highway operator. Vehicles pass plazas along one straight road; an entry read followed by an exit read forms a trip, and the trip is billed by distance. Customer support has three open fare complaints, and two billing features are queued behind them. All money is integer cents; fares are *computed* in tenths of a cent so per-km rates like 12.5 cents stay exact.
Codebase
| File | What it does |
|---|---|
toll/api.py | run(operations) entry point and the Dispatcher that validates and routes each operation |
toll/service.py | TollService: enrol vehicles, handle entry/exit reads, bill trips (_close) |
toll/trips.py | TripBook: open trips, closed trips, trip id allocation |
toll/tolls.py | The fare formula |
toll/money.py | Tenths-of-a-cent to cents conversion and the minimum-fare helper |
toll/highway.py | Plaza positions, distances, travel direction |
toll/registry.py | VehicleRegistry: which vehicles exist and what class they are billed as |
toll/statements.py | JSON shape of a trip (trip_view) |
toll/models.py, toll/errors.py, toll/config.py | Dataclasses, error codes, constants (plazas, rates, timeouts) |
tests/ | Existing unit tests (some currently fail) |
How your code is run
run(operations: list[list]) -> list runs the operations in order against a fresh system and returns one result per operation. Every operation is [name, ts, ...args]; ts is an integer number of seconds and never decreases. Errors are returned as "error: <code>" strings, never raised.
| Operation | Arguments after ts | Returns | Errors (checked in this order) |
|---|---|---|---|
enroll | vehicle, class | true | invalid class, vehicle exists |
entry | vehicle, plaza, direction | true | unknown plaza, invalid direction, unknown vehicle |
exit | vehicle, plaza | the closed trip (below) | unknown plaza, unknown vehicle, no open trip, wrong direction |
trips | vehicle | list of the vehicle's closed trips, in the order they were closed | unknown vehicle |
- The road: plazas
A0 km,B9,C26,D41,E77. Directions are"up"(towardsE) and"down"(towardsA). Classes and rates are intoll/config.py. - A wrong number of arguments, a non-integer
tsor a non-string/empty string argument returns"error: invalid arguments"; an unknown operation name returns"error: unknown operation". - A trip is a dict with exactly these keys:
id("t-1","t-2", ... in the order trips are *closed*),vehicle,from,to(plaza ids),km,fare(cents),fee(cents, see Part 3),kind,opened(ts of the entry),closed(ts the trip was billed). - A rejected operation changes nothing: no trip id is used and no state moves.
Worked example
[["enroll", 1, "T100", "car"],
["enroll", 2, "T200", "moto"],
["entry", 10, "T100", "B", "up"],
["exit", 30, "T100", "C"],
["entry", 40, "T200", "A", "up"],
["exit", 50, "T200", "B"],
["exit", 51, "T200", "B"],
["trips", 60, "T100"]]returns
[true, true, true,
{"id": "t-1", "vehicle": "T100", "from": "B", "to": "C", "km": 17, "fare": 213, "fee": 0, "kind": "normal", "opened": 10, "closed": 30},
true,
{"id": "t-2", "vehicle": "T200", "from": "A", "to": "B", "km": 9, "fare": 150, "fee": 0, "kind": "normal", "opened": 40, "closed": 50},
"error: no open trip",
[{"id": "t-1", "vehicle": "T100", "from": "B", "to": "C", "km": 17, "fare": 213, "fee": 0, "kind": "normal", "opened": 10, "closed": 30}]]Part 1 — Fix three fare complaints (TOLL-412, TOLL-418, TOLL-431)
Goal: make completed trips (kind "normal") bill and close exactly as specified.
Support tickets: "My 17 km trip cost 212 cents; the sign says 12.5 cents per km, so 212.5 should be 213." "My motorbike was charged 58 cents for a 9 km hop — I thought there was a minimum." "When the camera double-reads an exit we get two bills for one trip."
Rules
- Exact fare =
km x rate(tenths of a cent);kmis the distance between entry and exit plaza. - The minimum fare of
MIN_FARE_CENTSapplies to the exact fare: a trip is never billed less than 150 cents. - Whole cents = exact fare rounded half up (
x.5cents goes up). Never use floats. exitbills a trip only if the vehicle has an open trip, and billing closes it: a secondexitread returnsno open trip. Trip ids are given out at billing time.- The exit plaza must be at or beyond the entry plaza in the entry's direction (the same plaza is allowed, distance 0, minimum fare). Otherwise
wrong direction, and the trip stays open. entrywhile the vehicle already has an open trip is not defined yet (Part 2).
tests/test_fares.py and tests/test_service.py contain failing tests for these.
Part 2 — Missed exits and abandoned trips (TOLL-440)
Goal: a trip that never gets an exit read must still be billed, as a max-fare trip from its entry plaza to the *last plaza in its direction of travel* (E for "up", A for "down"), priced with the normal formula (class rate, minimum fare; an entry already at the last plaza is a 0 km trip at the minimum fare).
Rules
- Timeout. An open trip is billed as a max-fare trip with
kind"timeout"oncets >= opened + 43200(12 hours;OPEN_TRIP_TIMEOUT_S). Itsclosedisopened + 43200, not the ts of the operation that noticed. - Timeouts are checked before every operation whose
tsis an integer — reads, queries, failing and unknown operations included. Several expiring trips are billed in order ofopened, ties by the order they were opened, and take trip ids in that order. - Missed exit. A valid
entryfor a vehicle that still has an open trip first bills that old trip as a max-fare trip withkind"missed_exit"andclosed= the new entry'sts, then opens the new trip. Anentrythat is rejected leaves the old trip open. - An
exitthat arrives at or after the timeout finds no open trip (no open trip); one arriving earlier is a normal exit.exitstill returns the trip it closed. tripslists max-fare trips with the same shape;tois the last plaza reached.
Example (after enrolling T100 as a car)
[["entry", 100, "T100", "B", "up"],
["entry", 200, "T100", "C", "up"],
["exit", 43400, "T100", "D"],
["trips", 43401, "T100"]]returns true, true, "error: no open trip" and then two trips: t-1 from B to E (68 km, fare 850, missed_exit, opened 100, closed 200) and t-2 from C to E (51 km, fare 638, timeout, opened 200, closed 43400). The exit at 43400 is exactly 200 + 43200, so the timeout already billed the trip.
Part 3 — Camera-only vehicles and monthly statements (TOLL-455)
Goal: vehicles without a transponder are read by plate camera, pay an admin fee, and every vehicle gets a monthly statement.
New operations: register_plate (plate, class) returns true (errors in order: invalid class, plate registered); statement (period) returns the statement below (period must be an integer >= 0, otherwise invalid arguments). enroll of a vehicle id that starts with "plate:" returns invalid arguments.
Rules
- A vehicle id starting with
"plate:"is a camera read ("plate:XYZ9"is plateXYZ9). Plates need noenroll;unknown vehiclenever applies to them. Everything else (entry, exit, max-fare trips, timeouts) works exactly as before. - A plate trip is priced when it is billed, using the class the plate is registered under *at that moment*, else
car. A later registration never re-prices a trip. (A timed-out trip is billed *before* the operation that notices it runs, so aregister_platearriving after the timeout does not affect its price.) - Every plate trip owes a 150 cent
fee, unless the plate is registered with a registrationts <= closed + 604800(7 days). That includes plates registered before the trip. The fee is judged whenever a trip is shown, so registering a plate within a week of a trip waives that trip's fee retroactively. Transponder trips havefee0. statement(period)covers trips withperiod * 2592000 <= closed < (period + 1) * 2592000(30-day periods), by billing time (closed), never byopened.- The result is
{"period": p, "lines": [...]}with one line per vehicle that has trips in the period, sorted by vehicle id (plain string order):{"vehicle", "trips", "fares", "fees", "total"}.feesis the sum of the trips' current fees andtotal = fares + fees. faresis the sum of the exact fares (tenths) rounded half up once, so it can differ from the sum of the individual trips' roundedfarevalues by a few cents. Individual tripfarevalues keep Part 1's rounding.
Example
[["enroll", 1, "T100", "car"],
["entry", 100, "plate:XYZ9", "B", "up"],
["exit", 200, "plate:XYZ9", "C"],
["register_plate", 300, "XYZ9", "truck"],
["trips", 301, "plate:XYZ9"],
["entry", 400, "T100", "B", "up"], ["exit", 500, "T100", "C"],
["entry", 600, "T100", "B", "up"], ["exit", 700, "T100", "C"],
["statement", 800, 0]]The plate trip is first returned (by exit) with fare 213 and fee 150; after the registration the trips call shows the same fare 213 with fee 0. The statement is {"period": 0, "lines": [{"vehicle": "T100", "trips": 2, "fares": 425, "fees": 0, "total": 425}, {"vehicle": "plate:XYZ9", "trips": 1, "fares": 213, "fees": 0, "total": 213}]} — T100's two trips each show 213, but 2 x 212.5 is exactly 425.
Notes
highway.pyis known-good.- 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.
Example 1
- Input:
- [["enroll",1,"T100","car"],["entry",10,"T100","B","up"],["exit",30,"T100","C"],["exit",31,"T100","C"]]
- Output:
- [true,true,{"id":"t-1","vehicle":"T100","from":"B","to":"C","km":17,"fare":213,"fee":0,"kind":"normal","opened":10,"closed":30},"error: no open trip"]
- Explanation
- 17 km at 12.5 cents per km is exactly 212.5 cents, which rounds half up to 213. Billing closes the trip, so the repeated exit read is rejected.
Example 2
- Input:
- [["enroll",1,"T100","car"],["entry",100,"T100","B","up"],["entry",200,"T100","C","up"],["exit",43400,"T100","D"]]
- Output:
- [true,true,true,"error: no open trip"]
- Explanation
- The second entry bills the first trip as a max-fare trip from B to E. The trip opened at 200 times out at exactly 43400, so the exit at 43400 finds nothing open.
Guidelines
- Python 3 standard library only
- Money is integer cents; fares are computed in integer tenths of a cent and there is no floating-point money anywhere
- Timestamps are integer seconds and never decrease from one operation to the next
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- A rejected operation changes nothing, including trip ids