Fleet Maintenance: Service Intervals, Work Orders and Forecasts
MediumAI-assisted coding · AI-Assisted Debugging
Fleet Maintenance: Service Intervals, Work Orders and Forecasts
You've joined the depot-systems team at a delivery company. Their fleet maintenance tracker decides when each van is due for service, tracks the shop's work orders and parts, and tells dispatch which vans may go out. Last week two vans went out with lapsed services and a parts count drifted from the shelf. You'll fix what ops reported, finish the work-order and dispatch rules, and then absorb a requirements change from the fleet manager.
Codebase
| File | What it does |
|---|---|
fleet/api.py | run(operations) entry point and the Router that parses dates and dispatches each operation |
fleet/vehicles.py | FleetRegistry: vehicles, odometer readings, service records, the due report |
fleet/intervals.py | Computes miles left, due date and status for one vehicle and service type |
fleet/dates.py | Date parsing/formatting and add_months |
fleet/config.py | Service intervals, due-soon windows, the grounding service, bundles |
fleet/models.py | Vehicle, ServiceRecord, WorkOrder dataclasses |
fleet/inventory.py | PartsInventory: on-hand and reserved counts per part |
fleet/workorders.py | WorkOrders: open, close and cancel work orders |
fleet/grounding.py | GroundingPolicy.dispatch (stub for Part 2) |
fleet/forecast.py | Forecaster: drive, forecast, next_due (stub for Part 3) |
fleet/errors.py | FleetError and every error code |
tests/ | Existing unit and end-to-end tests (some currently fail) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh depot and returns one result per operation. Every operation is [name, date, ...args]. date is the day the operation happens, a "YYYY-MM-DD" string, and never decreases from one operation to the next. Errors are returned as strings ("error: <code>"), never raised.
- An unknown operation name returns
"error: unknown operation". Adatethat is not a real calendar date inYYYY-MM-DDform, or a wrong number of arguments, returns"error: invalid arguments". - Vehicle ids, part names and work-order ids are case-sensitive strings.
- A *reading* is a whole number of miles
>= 0(true/falseare not numbers). - Dates in results are
YYYY-MM-DDstrings.
Part 1 operations (all but due return the value shown)
| Operation | Arguments after date | Returns | Errors (checked in this order) |
|---|---|---|---|
add_vehicle | vehicle_id, reading | true | invalid arguments (empty/non-string id), invalid reading, vehicle exists |
odometer | vehicle_id, reading | the reading | vehicle not found, invalid reading, odometer decreased |
replace_odometer | vehicle_id, reading | true | vehicle not found, invalid reading |
service | vehicle_id, service | true | vehicle not found, unknown service |
due | vehicle_id | list of [service, status, miles_left, due_date] | vehicle not found |
Part 1: Fix the "due" report (INC-4417)
Goal: make due (and the readings behind it) follow the rules below. Ops reported: a van was flagged merely "due soon" when its oil change was a day late; a van with a swapped odometer showed nonsense mileage; the 6-month interval from a Jan 31 service landed on the wrong day; and a driver's second reading of the same mileage was rejected.
Rules
- Service types and intervals live in
fleet/config.py(oil_change5000 mi / 6 mo,tire_rotation7500 / 12,brake_service30000 / 24,safety_inspection12000 / 12). A service is due when either limit is reached, whichever comes first:interval_milesdriven orinterval_monthselapsed since it was last performed. - A new vehicle counts as having had every service type performed on its registration date at its registration reading.
- Lifetime miles never go backwards.
odometeraccepts a reading>=the current one (an equal reading is fine) and rejects a lower one withodometer decreased.replace_odometerrecords a gauge swap: the new gauge starts at the given reading, which may be lower or higher, and lifetime miles carry on unchanged (a gauge that read 80,000 and is replaced by one reading 50 means lifetime miles are still 80,000, then 80,001 at a reading of 51). After a swap,odometerreadings are compared against the new gauge. Everything that measures miles (miles since a service, miles left) uses lifetime miles, never the raw gauge. miles_left = interval_miles - (lifetime miles now - lifetime miles at the last service). It is negative once the limit is passed.due_dateis the last service date plusinterval_monthscalendar months: same day of the month, clamped to the last day of the target month if it is shorter (Jan 31 + 1 month is Feb 28, or Feb 29 in a leap year; Feb 29 2028 + 12 months is Feb 28 2029).days_left = due_date - todayin days. For each of the two measures, an amount< 0is overdue,<= 500miles (or<= 14days) is due soon, anything else is ok (so exactly 0 left is *due soon*, not overdue). The service's status is the more severe of the two (overdue>due_soon>ok).duereturns one row per service type, sorted by service name (plain string order).
Example
[["add_vehicle", "2027-08-31", "van-1", 50000],
["odometer", "2027-12-01", "van-1", 54620],
["due", "2028-03-01", "van-1"]]returns
[true, 54620,
[["brake_service", "ok", 25380, "2029-08-31"],
["oil_change", "overdue", 380, "2028-02-29"],
["safety_inspection", "ok", 7380, "2028-08-31"],
["tire_rotation", "ok", 2880, "2028-08-31"]]]Oil change is 380 miles from its limit but its 6 months ended on 2028-02-29, so it is overdue today.
Part 2: Work orders, parts and grounding (OPS-912)
Goal: the shop opens a work order for a service, reserves the parts it needs, then closes it (parts fitted, service recorded) or cancels it (parts go back on the shelf). Parts counts must always match the shelf. Dispatch must refuse vans whose safety inspection has lapsed.
| Operation | Arguments after date | Returns | Errors (checked in this order) |
|---|---|---|---|
stock | part, quantity | units now available | invalid arguments (empty/non-string part), invalid quantity (not an int > 0) |
stock_report | part | [on_hand, reserved, available] | invalid arguments |
open_wo | vehicle_id, service, parts | work-order id | vehicle not found, unknown service, invalid parts, service already open, insufficient stock |
close_wo | wo_id | true | work order not found, work order not open |
cancel_wo | wo_id | true | work order not found, work order not open |
dispatch | vehicle_id | true | vehicle not found, vehicle grounded |
Rules
available = on_hand - reserved.stockadds toon_hand. A part never stocked reports[0, 0, 0].partsis a list of[part, quantity]lines (it may be empty); a line that isn't a 2-item list of a non-empty string and an int> 0makes the whole listinvalid parts. Lines naming the same part are summed before anything else happens.- Opening reserves every part, all or nothing: if any part's total exceeds its
available(a part never stocked has 0), returninsufficient stockand change nothing, including the id counter. - Work-order ids are
WO-1,WO-2, ... in the order orders are opened successfully. - A vehicle can have only one open work order per service type (
service already open); other vehicles, or a service whose earlier order is closed or cancelled, are unaffected. close_wofitting the parts: each reserved unit leaves bothreservedandon_hand. It also records the service as performed on the close date at the vehicle's current lifetime miles, exactly likeservice(so it resets that interval). The order is then closed.cancel_woreleases every reservation (reserveddrops,on_handis unchanged); no service is recorded. Only an open order can be closed or cancelled.- A vehicle is grounded when its
safety_inspection(the type named byGROUNDING_SERVICE) status isoverdueon the dispatch date. A grounded vehicle getsvehicle grounded; any other vehicle, including one that is onlydue_soon, is dispatched. Dispatching changes nothing.
Example
[["add_vehicle", "2026-01-10", "van-1", 1000],
["stock", "2026-01-10", "oil-filter", 3],
["open_wo", "2026-02-01", "van-1", "oil_change", [["oil-filter", 2]]],
["stock_report", "2026-02-01", "oil-filter"],
["cancel_wo", "2026-02-02", "WO-1"],
["stock_report", "2026-02-02", "oil-filter"],
["dispatch", "2027-01-10", "van-1"],
["dispatch", "2027-01-11", "van-1"]]returns
[true, 3, "WO-1", [3, 2, 1], true, [3, 0, 3], true, "error: vehicle grounded"](Registered 2026-01-10, the 12-month inspection is due on 2027-01-10: still only "due soon" that day, overdue the day after.)
Part 3: Requirements change: bundles and projected due dates (FLEET-77)
The fleet manager wants shop packages and a planning view. Two things change.
3a. Service bundles
A bundle performs several service types at once. fleet/config.py gets:
| Bundle | Performs |
|---|---|
A | oil_change, tire_rotation |
annual | oil_change, safety_inspection, tire_rotation |
serviceandopen_woaccept a bundle name wherever they take a service name. Performing a bundle records every component as performed on that day at the current lifetime miles (each interval resets). A name that is neither a service nor a bundle isunknown service.- A bundle work order, when closed, records all its components. When cancelled it records nothing. The
partson a bundle order are just the parts list given, reserved and consumed once. - This replaces Part 2 rule 5. A vehicle can no longer have two open work orders that share a component service type: opening
Awhileoil_changeis open (orannualwhileAis open, ortire_rotationwhileAis open) returnsservice already open.brake_servicealongsideAis fine. - Everything from Parts 1 and 2 still applies to bundles' components: closing an
annualorder on a grounded van un-grounds it.
3b. Forecasts
| Operation | Arguments after date | Returns | Errors (checked in this order) |
|---|---|---|---|
drive | vehicle_id, days, pattern | the raw odometer after the last day | vehicle not found, invalid arguments |
forecast | vehicle_id | list of [service, estimated_due_date], sorted by service name | vehicle not found |
next_due | count | the count earliest [vehicle_id, service, estimated_due_date] across the fleet | invalid arguments (not an int >= 1) |
- Reading history. Every reading is logged with its date in lifetime miles: the registration reading (on its date), every accepted
odometerreading, and every day of adrive.replace_odometerlogs nothing (lifetime miles don't change). drivelogs one reading per day fordaysconsecutive days starting ondate, where the gauge increases each day by the next number ofpattern, repeating the pattern from the start ifdaysis longer.daysmust be an int>= 1andpatterna non-empty list of ints>= 0(otherwiseinvalid arguments). Operations after adriveare dated on or after its last day.- Average daily miles on date
D: look at the logged readings dated fromD - 30 daysthroughD(inclusive both ends). If there are fewer than 2 of them, or the first and last of them are on the same date, the average is0. Otherwiseaverage = (last_miles - first_miles) // (days between their dates), integer floor, where *first* is the earliest-logged reading in the window and *last* the most recent reading dated<= D. - Estimated due date for one service on date
D: letleftbemiles_leftfrom Part 1 anddue_datethe month-based due date. Ifleft <= 0the mileage estimate isDitself. Else ifaverage > 0it isD + ceil(left / average)days (integer math:(left + average - 1) // average). Ifaverageis0there is no mileage estimate. The estimate is the earlier ofdue_dateand the mileage estimate (adue_datealready in the past is reported as is). forecastlists every service type (not bundles).next_dueconsiders every vehicle and service type, sorts by(estimated_due_date, vehicle_id, service)and returns the firstcount(all of them if fewer exist).- Hidden tests drive dozens of vans for tens of thousands of days each and call
next_duehundreds of times in onerun; the whole suite gets a few seconds. Rescanning a vehicle's whole history for each call is too slow.
Example
[["add_vehicle", "2028-01-01", "van-1", 0],
["drive", "2028-01-01", "van-1", 5, [100, 200]],
["forecast", "2028-01-05", "van-1"],
["service", "2028-01-05", "van-1", "A"],
["due", "2028-01-05", "van-1"]]returns
[true, 700,
[["brake_service", "2028-06-21"], ["oil_change", "2028-01-30"],
["safety_inspection", "2028-03-10"], ["tire_rotation", "2028-02-13"]],
true,
[["brake_service", "ok", 29300, "2030-01-01"], ["oil_change", "ok", 5000, "2028-07-05"],
["safety_inspection", "ok", 11300, "2029-01-01"], ["tire_rotation", "ok", 7500, "2029-01-05"]]]The average is (700 - 0) // 4 = 175 miles/day (registration reading 0 on Jan 1, 700 on Jan 5), so the oil change (4300 miles left) is ceil(4300 / 175) = 25 days out. After bundle A, oil change and tire rotation have reset; brake service and the inspection have not.
Notes
dates.pyandconfig.pyare the only places that know about calendars and intervals; keep it that way.- 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 behavior after later changes.
Example 1
- Input:
- [["add_vehicle", "2027-08-31", "van-1", 50000], ["odometer", "2027-12-01", "van-1", 54620], ["due", "2028-03-01", "van-1"]]
- Output:
- [true, 54620, [["brake_service", "ok", 25380, "2029-08-31"], ["oil_change", "overdue", 380, "2028-02-29"], ["safety_inspection", "ok", 7380, "2028-08-31"], ["tire_rotation", "ok", 2880, "2028-08-31"]]]
- Explanation
- Oil change has 380 miles left, but six calendar months from 2027-08-31 clamp to 2028-02-29, which has passed on 2028-03-01, so it is overdue. The more severe measure wins.
Example 2
- Input:
- [["add_vehicle", "2026-01-10", "van-1", 1000], ["stock", "2026-01-10", "oil-filter", 3], ["open_wo", "2026-02-01", "van-1", "oil_change", [["oil-filter", 2]]], ["stock_report", "2026-02-01", "oil-filter"], ["cancel_wo", "2026-02-02", "WO-1"], ["stock_report", "2026-02-02", "oil-filter"], ["dispatch", "2027-01-10", "van-1"], ["dispatch", "2027-01-11", "van-1"]]
- Output:
- [true, 3, "WO-1", [3, 2, 1], true, [3, 0, 3], true, "error: vehicle grounded"]
- Explanation
- Opening reserves two filters; cancelling releases them. The safety inspection is only due soon on 2027-01-10 (dispatch allowed) and overdue the day after (grounded).
Example 3
- Input:
- [["add_vehicle", "2028-01-01", "van-1", 0], ["drive", "2028-01-01", "van-1", 5, [100, 200]], ["forecast", "2028-01-05", "van-1"], ["service", "2028-01-05", "van-1", "A"]]
- Output:
- [true, 700, [["brake_service", "2028-06-21"], ["oil_change", "2028-01-30"], ["safety_inspection", "2028-03-10"], ["tire_rotation", "2028-02-13"]], true]
- Explanation
- Average is (700-0)//4 = 175 miles/day, so the oil change (4300 miles left) is ceil(4300/175) = 25 days out. Bundle A then resets oil change and tire rotation together.
Guidelines
- Python 3 standard library only
- Dates are YYYY-MM-DD strings and never decrease from one operation to the next
- Mileage is whole miles; all averages and projections use the exact integer math in the README
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Part 3 inputs contain hundreds of thousands of logged odometer readings and dozens of fleet-wide queries