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>
This commit is contained in:
NuklearRabbit
2026-08-03 21:37:34 +02:00
co-authored by Claude Sonnet 5
parent 18344bc8b7
commit 6deb95524d
68 changed files with 1734 additions and 225 deletions
+218
View File
@@ -0,0 +1,218 @@
"""The single authoritative vehicle-status evaluator.
Used by the data-quality scanner (detection), the status-recommendation preview
endpoint, the apply endpoint, and tests -- so scan-time detection and resolve-time
recommendation can never structurally disagree (see docs/fleet-ops-correction/
vehicle-status-decision-table.md for the full decision table and rationale).
The evaluator only ever reasons from real, freshly-queried domain facts (an actually
active rental, a real service-threshold breach, a real overlapping-booking conflict) --
never from a proxy like "does some other high-severity issue happen to be open". It is
therefore also order-independent: resolving, deferring or rejecting an unrelated issue on
the same vehicle never changes what this function returns, because it never looks at
issue history, only at the vehicle's/bookings' current state.
"""
from __future__ import annotations
import hashlib
import json
import uuid
from dataclasses import dataclass, field
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models.booking import Booking
from app.models.data_quality import DataQualityIssue
from app.models.vehicle import Vehicle
# Every code below is a stable, localizable identifier -- see
# frontend/src/i18n/messageCodes.ts and quality:statusRecommendation.codes.* for the
# human-language mapping in all three supported locales. The backend never emits prose.
RECOMMENDATION_CODE_ACTIVE_RENTAL = "vehicle.active_rental"
RECOMMENDATION_CODE_SERVICE_THRESHOLD = "vehicle.service_threshold_reached"
RECOMMENDATION_CODE_BOOKING_CONFLICT = "vehicle.booking_conflict"
RECOMMENDATION_CODE_RENTAL_ENDED = "vehicle.rental_ended"
RECOMMENDATION_CODE_MANUAL_REVIEW = "vehicle.manual_review_required"
RECOMMENDATION_CODE_NO_CONFLICT = "vehicle.no_conflict"
@dataclass
class VehicleStatusFacts:
active_booking_refs: list[str] = field(default_factory=list)
overlapping_booking_pairs: list[tuple[str, str]] = field(default_factory=list)
service_threshold_reached: bool = False
odometer_km: int = 0
next_service_km: int = 0
open_booking_overlap_issue_ref: str | None = None
@property
def has_active_rental(self) -> bool:
return len(self.active_booking_refs) > 0
@property
def has_booking_conflict(self) -> bool:
return (
len(self.overlapping_booking_pairs) > 0
or self.open_booking_overlap_issue_ref is not None
)
def as_dict(self) -> dict:
return {
"active_booking_refs": self.active_booking_refs,
"overlapping_booking_pairs": [list(pair) for pair in self.overlapping_booking_pairs],
"service_threshold_reached": self.service_threshold_reached,
"odometer_km": self.odometer_km,
"next_service_km": self.next_service_km,
"open_booking_overlap_issue_ref": self.open_booking_overlap_issue_ref,
}
@dataclass
class VehicleStatusRecommendation:
current_status: str
recommended_status: str | None
recommendation_code: str
safe_to_apply: bool
manual_review_required: bool
facts: VehicleStatusFacts
blocking_reasons: list[str]
def _overlapping_booking_pairs(bookings: list[Booking]) -> list[tuple[Booking, Booking]]:
ordered = sorted(bookings, key=lambda b: b.starts_at)
pairs: list[tuple[Booking, Booking]] = []
for i, first in enumerate(ordered):
for second in ordered[i + 1 :]:
if second.starts_at < first.ends_at and first.starts_at < second.ends_at:
pairs.append((first, second))
return pairs
def gather_vehicle_status_facts(
db: Session, vehicle: Vehicle, *, exclude_issue_id: uuid.UUID | None = None
) -> VehicleStatusFacts:
"""Real, freshly-queried facts only -- see module docstring. Never cached, never
derived from another issue's mere existence (only a *specific* booking_overlap
issue's presence is used, as a cross-reference to that issue's own public_ref)."""
reserved_or_active = list(
db.scalars(
select(Booking).where(
Booking.vehicle_id == vehicle.id,
Booking.status.in_(["reserved", "active"]),
)
).all()
)
active_refs = [b.public_ref for b in reserved_or_active if b.status == "active"]
overlap_pairs = [
(a.public_ref, b.public_ref) for a, b in _overlapping_booking_pairs(reserved_or_active)
]
overlap_issue_query = select(DataQualityIssue.public_ref).where(
DataQualityIssue.entity_type == "vehicle",
DataQualityIssue.entity_id == vehicle.id,
DataQualityIssue.status == "open",
DataQualityIssue.rule_type == "booking_overlap",
)
if exclude_issue_id is not None:
overlap_issue_query = overlap_issue_query.where(DataQualityIssue.id != exclude_issue_id)
open_overlap_ref = db.scalar(overlap_issue_query)
return VehicleStatusFacts(
active_booking_refs=active_refs,
overlapping_booking_pairs=overlap_pairs,
service_threshold_reached=vehicle.odometer_km >= vehicle.next_service_km,
odometer_km=vehicle.odometer_km,
next_service_km=vehicle.next_service_km,
open_booking_overlap_issue_ref=open_overlap_ref,
)
def compute_recommendation_token(vehicle: Vehicle, facts: VehicleStatusFacts) -> str:
"""A short digest of exactly the facts the recommendation was based on, plus the
vehicle's optimistic-lock version. The apply endpoint recomputes this from fresh
facts and rejects the request if it doesn't match the token the client last saw --
the frontend must never assume a previously-shown preview is still valid without the
server re-checking it (see docs/fleet-ops-correction/current-gap-audit.md §8F)."""
payload = {"version": vehicle.version, "status": vehicle.operational_status, **facts.as_dict()}
digest = hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode()).hexdigest()
return digest[:16]
def evaluate_vehicle_status(
vehicle: Vehicle, facts: VehicleStatusFacts
) -> VehicleStatusRecommendation:
"""Pure decision logic over already-gathered facts -- see
docs/fleet-ops-correction/vehicle-status-decision-table.md. Never mutates anything,
never queries the database itself (call gather_vehicle_status_facts first), so it is
trivial to unit-test every branch in isolation."""
current = vehicle.operational_status
blocking_reasons: list[str] = []
if facts.service_threshold_reached:
blocking_reasons.append(RECOMMENDATION_CODE_SERVICE_THRESHOLD)
if facts.has_booking_conflict:
blocking_reasons.append(RECOMMENDATION_CODE_BOOKING_CONFLICT)
if current == "maintenance" and RECOMMENDATION_CODE_SERVICE_THRESHOLD not in blocking_reasons:
# Already being in maintenance is itself a real blocking fact -- an active
# booking never overrides it. This is exactly the forbidden shortcut this
# evaluator must never take (maintenance + active booking -> auto "rented").
blocking_reasons.append(RECOMMENDATION_CODE_SERVICE_THRESHOLD)
def result(
recommended: str | None, code: str, *, safe: bool, manual: bool
) -> VehicleStatusRecommendation:
return VehicleStatusRecommendation(
current_status=current,
recommended_status=recommended,
recommendation_code=code,
safe_to_apply=safe,
manual_review_required=manual,
facts=facts,
blocking_reasons=blocking_reasons,
)
if facts.has_active_rental and not blocking_reasons:
if current == "rented":
return result(None, RECOMMENDATION_CODE_NO_CONFLICT, safe=False, manual=False)
return result(
"rented", RECOMMENDATION_CODE_ACTIVE_RENTAL, safe=True, manual=False
)
if facts.has_active_rental and blocking_reasons:
# Explicitly forbidden shortcut this evaluator must never take: an active
# booking is not proof the vehicle should be "rented" when a real blocking
# condition also exists (e.g. maintenance-due, or a genuine booking conflict).
# This is a real contradiction in the underlying facts, not something safe to
# resolve automatically.
return result(None, RECOMMENDATION_CODE_MANUAL_REVIEW, safe=False, manual=True)
if facts.service_threshold_reached:
if current == "maintenance":
return result(None, RECOMMENDATION_CODE_NO_CONFLICT, safe=False, manual=False)
return result(
"maintenance", RECOMMENDATION_CODE_SERVICE_THRESHOLD, safe=True, manual=False
)
if facts.has_booking_conflict:
if current == "blocked":
return result(None, RECOMMENDATION_CODE_NO_CONFLICT, safe=False, manual=False)
return result(
"blocked", RECOMMENDATION_CODE_BOOKING_CONFLICT, safe=True, manual=False
)
# No active rental, no maintenance need, no booking conflict.
if current in ("available", "cleaning", "blocked"):
return result(None, RECOMMENDATION_CODE_NO_CONFLICT, safe=False, manual=False)
if current == "rented":
return result(
"available", RECOMMENDATION_CODE_RENTAL_ENDED, safe=True, manual=False
)
if current == "maintenance":
# No positive fact confirms maintenance is actually finished (no completed
# service record is tracked here) -- clearing "maintenance" without such a
# fact would be exactly the kind of unsafe shortcut this evaluator forbids.
# Releasing a vehicle from maintenance remains an explicit, manual decision.
return result(None, RECOMMENDATION_CODE_NO_CONFLICT, safe=False, manual=False)
return result(None, RECOMMENDATION_CODE_MANUAL_REVIEW, safe=False, manual=True)