# Vehicle status decision table Authoritative rationale for `app/services/vehicle_status.py::evaluate_vehicle_status`, the single evaluator shared by the data-quality scanner, the status-recommendation preview endpoint, and the transactional apply endpoint (section 8A). Scanner and resolver call the same function with the same freshly-gathered facts, so they can never disagree, and the recommendation is order-independent: resolving, deferring, or rejecting an unrelated issue never changes what this function returns for a vehicle, because it only reasons over the vehicle's and bookings' current state, never over issue history. ## Facts gathered (`gather_vehicle_status_facts`) All facts are re-queried from the database on every call, never cached and never derived from "does some other issue happen to be open": | Fact | Source | |---|---| | `active_booking_refs` | Bookings on this vehicle with `status == "active"` | | `overlapping_booking_pairs` | Reserved/active bookings on this vehicle whose date ranges genuinely overlap | | `service_threshold_reached` | `vehicle.odometer_km >= vehicle.next_service_km` | | `open_booking_overlap_issue_ref` | The `public_ref` of a currently-open `booking_overlap` issue on this vehicle, if any (excluding the issue being resolved, via `exclude_issue_id`) | `has_active_rental` = at least one active booking. `has_booking_conflict` = an overlapping-booking pair exists, or an open `booking_overlap` issue references this vehicle. ## Decision table | Current status | Active rental? | Service threshold reached? | Booking conflict? | Recommended status | Priority | `recommendation_code` | Safe to auto-apply? | |---|---|---|---|---|---|---|---| | any except `maintenance` | yes | no | no | `rented` (if not already) | 1 | `vehicle.active_rental` | yes | | `maintenance` | yes | — | — | *(none — manual review)* | 1 | `vehicle.manual_review_required` | no | | any | yes | yes | — | *(none — manual review)* | 1 | `vehicle.manual_review_required` | no | | any | yes | — | yes | *(none — manual review)* | 1 | `vehicle.manual_review_required` | no | | not `maintenance` | no | yes | — | `maintenance` | 2 | `vehicle.service_threshold_reached` | yes | | `maintenance` | no | yes | — | *(none — already correct)* | 2 | `vehicle.no_conflict` | n/a | | not `blocked` | no | no | yes | `blocked` | 3 | `vehicle.booking_conflict` | yes | | `blocked` | no | no | yes | *(none — already correct)* | 3 | `vehicle.no_conflict` | n/a | | `rented` | no | no | no | `available` | 4 | `vehicle.rental_ended` | yes | | `available` / `cleaning` / `blocked` | no | no | no | *(none — already correct)* | 4 | `vehicle.no_conflict` | n/a | | `maintenance` | no | no | no | *(none — stays in maintenance)* | 4 | `vehicle.no_conflict` | n/a | | anything not covered above | — | — | — | *(none — manual review)* | 5 | `vehicle.manual_review_required` | no | ## Safe-status principles (section 8B) applied - **Maintenance + active booking never auto-resolves to `rented`.** Being currently in `maintenance` is itself treated as a blocking fact (row 2 above) — an active booking is never proof the vehicle should be marked rented; it is a real contradiction that requires a human to investigate (e.g. was the vehicle released from the workshop without updating status, or is the booking itself stale). - **`available` + any booking never auto-resolves to `rented` silently past a real blocker.** The `rented` recommendation only fires when there is no competing blocking fact (no service-threshold breach, no booking conflict, not already in maintenance). - **An open issue disappearing never auto-resolves to `available`.** Leaving `maintenance` requires a human decision — this evaluator holds no fact that proves maintenance work is actually finished (no completed-service record is modelled), so a vehicle sitting in `maintenance` with no active rental and no other blocker stays `vehicle.no_conflict` (left alone) rather than being auto-promoted to `available`. - **Every branch re-derives facts; nothing is cached.** `blocking_reasons` is always computed fresh from `service_threshold_reached`, `has_booking_conflict`, and the current status itself — never from a proxy like "is some other high-severity issue still open". ## Statuses used Only statuses that exist in the current domain model are referenced: `available`, `rented`, `cleaning`, `maintenance`, `blocked`. `cleaning` is never a recommendation target from this evaluator (no fact here proves cleaning is required or complete); it is only ever an input `current_status` that, absent any blocker, is left alone (`vehicle.no_conflict`). ## Concurrency: recommendation token `compute_recommendation_token(vehicle, facts)` hashes the vehicle's optimistic-lock `version` plus every fact the recommendation was based on (sha256, truncated to 16 hex chars). The preview endpoint returns this token; the apply endpoint recomputes it from freshly-gathered facts inside the same transaction and rejects the request (`RECOMMENDATION_STALE`) if it no longer matches — the frontend must never assume a previously-shown preview is still valid without server revalidation (section 8F). ## MO-016: order independence MO-016 carries both an open `booking_overlap` issue and an open `vehicle_status_conflict` issue at once (two overlapping reserved bookings). Because `gather_vehicle_status_facts` re-queries `overlapping_booking_pairs` and `open_booking_overlap_issue_ref` fresh every call, resolving the booking-overlap issue first vs. resolving the status-conflict issue first both converge on the same final vehicle status — see `test_mo_016_status_conflict_recommendation_is_order_independent` in `backend/tests/test_data_quality.py`.