Files
MobilityOps/docs/fleet-ops-correction/vehicle-status-decision-table.md
NuklearRabbitandClaude Sonnet 5 6deb95524d fix: safe status-recommendation flow, MO-016 order independence, brand constant, message codes
- Add a single shared, pure vehicle-status evaluator (app/services/vehicle_status.py)
  used identically by the data-quality scanner, a new non-mutating status-recommendation
  preview endpoint, and a transactional apply endpoint with optimistic-concurrency token
  revalidation -- eliminates the old opaque "calculate and apply" action and the unsafe
  "maintenance + active booking -> auto rented" shortcut. Frontend
  DataQualityIssueDetail.tsx now shows a review/decide/confirm panel with localized
  why/evidence/consequence text in nl-BE/en-GB/fr-BE, with an exact "Change status to
  <status>" confirm action per the brief.
- Fix MO-016 issue-order dependency: resolving the booking-overlap issue before vs.
  after the status-conflict issue now converges on the same final vehicle status,
  proven by test_mo_016_status_conflict_recommendation_is_order_independent.
- Make "Fleet Ops" a non-localizable brand constant (frontend/src/product.ts,
  backend PRODUCT_NAME) via {{productName}} interpolation everywhere the brand name
  appeared in locale prose; add a permanent test guarding against a translation file
  ever defining the brand name or an "appName" key again.
- Convert dynamic backend prose to stable message codes + params: return status
  reasons, audit field/actor-type labels, automation last_error, and search
  section/vehicle/booking/issue results all now carry codes the frontend localizes,
  with raw technical text demoted to a "Technical details" disclosure.
- docs/fleet-ops-correction/: gap audit, i18n inventory, and the vehicle-status
  decision table documenting the evaluator's rules and safe-status principles.

148 backend tests + Ruff + mypy green; Alembic migration verified upgrade/downgrade;
frontend tsc/build and the i18n-coverage Playwright suite green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 21:37:34 +02:00

5.7 KiB

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.