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:
co-authored by
Claude Sonnet 5
parent
18344bc8b7
commit
6deb95524d
@@ -0,0 +1,160 @@
|
||||
"""Pure unit tests for the shared vehicle-status evaluator -- no database needed, since
|
||||
evaluate_vehicle_status() only reasons over an already-gathered VehicleStatusFacts. See
|
||||
docs/fleet-ops-correction/vehicle-status-decision-table.md for the decision table these
|
||||
tests are asserting against."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.services.vehicle_status import (
|
||||
RECOMMENDATION_CODE_ACTIVE_RENTAL,
|
||||
RECOMMENDATION_CODE_BOOKING_CONFLICT,
|
||||
RECOMMENDATION_CODE_MANUAL_REVIEW,
|
||||
RECOMMENDATION_CODE_NO_CONFLICT,
|
||||
RECOMMENDATION_CODE_RENTAL_ENDED,
|
||||
RECOMMENDATION_CODE_SERVICE_THRESHOLD,
|
||||
VehicleStatusFacts,
|
||||
compute_recommendation_token,
|
||||
evaluate_vehicle_status,
|
||||
)
|
||||
|
||||
|
||||
def _vehicle(status: str, *, version: int = 1):
|
||||
return SimpleNamespace(operational_status=status, version=version)
|
||||
|
||||
|
||||
def _facts(**overrides) -> VehicleStatusFacts:
|
||||
defaults = dict(
|
||||
active_booking_refs=[],
|
||||
overlapping_booking_pairs=[],
|
||||
service_threshold_reached=False,
|
||||
odometer_km=10_000,
|
||||
next_service_km=20_000,
|
||||
open_booking_overlap_issue_ref=None,
|
||||
)
|
||||
defaults.update(overrides)
|
||||
return VehicleStatusFacts(**defaults)
|
||||
|
||||
|
||||
def test_available_with_active_rental_recommends_rented():
|
||||
result = evaluate_vehicle_status(
|
||||
_vehicle("available"), _facts(active_booking_refs=["BK-0001"])
|
||||
)
|
||||
assert result.recommended_status == "rented"
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_ACTIVE_RENTAL
|
||||
assert result.safe_to_apply is True
|
||||
assert result.manual_review_required is False
|
||||
|
||||
|
||||
def test_maintenance_with_active_rental_never_auto_recommends_rented():
|
||||
# The exact unsafe shortcut this task explicitly forbids: maintenance + an active
|
||||
# booking must NEVER be auto-resolved to "rented".
|
||||
result = evaluate_vehicle_status(
|
||||
_vehicle("maintenance"), _facts(active_booking_refs=["BK-0001"])
|
||||
)
|
||||
assert result.recommended_status is None
|
||||
assert result.manual_review_required is True
|
||||
assert result.safe_to_apply is False
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_MANUAL_REVIEW
|
||||
|
||||
|
||||
def test_available_with_active_rental_and_service_threshold_requires_manual_review():
|
||||
result = evaluate_vehicle_status(
|
||||
_vehicle("available"),
|
||||
_facts(active_booking_refs=["BK-0001"], service_threshold_reached=True),
|
||||
)
|
||||
assert result.manual_review_required is True
|
||||
assert result.recommended_status is None
|
||||
|
||||
|
||||
def test_available_with_active_rental_and_booking_conflict_requires_manual_review():
|
||||
result = evaluate_vehicle_status(
|
||||
_vehicle("available"),
|
||||
_facts(active_booking_refs=["BK-0001"], open_booking_overlap_issue_ref="DQ-0001"),
|
||||
)
|
||||
assert result.manual_review_required is True
|
||||
assert result.recommended_status is None
|
||||
|
||||
|
||||
def test_rented_with_no_active_booking_recommends_available():
|
||||
result = evaluate_vehicle_status(_vehicle("rented"), _facts())
|
||||
assert result.recommended_status == "available"
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_RENTAL_ENDED
|
||||
assert result.safe_to_apply is True
|
||||
|
||||
|
||||
def test_service_threshold_reached_recommends_maintenance():
|
||||
result = evaluate_vehicle_status(
|
||||
_vehicle("available"), _facts(service_threshold_reached=True)
|
||||
)
|
||||
assert result.recommended_status == "maintenance"
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_SERVICE_THRESHOLD
|
||||
|
||||
|
||||
def test_booking_conflict_recommends_blocked_not_a_generic_high_severity_proxy():
|
||||
# The evaluator must react to a *real* booking-conflict fact, not "does some other
|
||||
# open high-severity issue happen to exist" (the forbidden proxy).
|
||||
result = evaluate_vehicle_status(
|
||||
_vehicle("available"),
|
||||
_facts(overlapping_booking_pairs=[("BK-DEMO-OVERLAP-A", "BK-DEMO-OVERLAP-B")]),
|
||||
)
|
||||
assert result.recommended_status == "blocked"
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_BOOKING_CONFLICT
|
||||
|
||||
|
||||
def test_open_booking_overlap_issue_alone_also_triggers_blocked():
|
||||
result = evaluate_vehicle_status(
|
||||
_vehicle("available"), _facts(open_booking_overlap_issue_ref="DQ-DEMO-OVERLAP")
|
||||
)
|
||||
assert result.recommended_status == "blocked"
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_BOOKING_CONFLICT
|
||||
|
||||
|
||||
def test_maintenance_with_no_active_rental_and_no_blockers_stays_manual():
|
||||
# No fact here confirms maintenance is actually finished, so the evaluator must not
|
||||
# auto-clear it to "available" -- that release remains an explicit, manual decision.
|
||||
result = evaluate_vehicle_status(_vehicle("maintenance"), _facts())
|
||||
assert result.recommended_status is None
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_NO_CONFLICT
|
||||
assert result.safe_to_apply is False
|
||||
|
||||
|
||||
def test_no_conflict_when_status_already_matches_facts():
|
||||
result = evaluate_vehicle_status(_vehicle("available"), _facts())
|
||||
assert result.recommended_status is None
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_NO_CONFLICT
|
||||
assert result.safe_to_apply is False
|
||||
|
||||
result = evaluate_vehicle_status(_vehicle("rented"), _facts(active_booking_refs=["BK-1"]))
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_NO_CONFLICT
|
||||
|
||||
result = evaluate_vehicle_status(_vehicle("blocked"), _facts())
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_NO_CONFLICT
|
||||
|
||||
result = evaluate_vehicle_status(
|
||||
_vehicle("maintenance"), _facts(service_threshold_reached=True)
|
||||
)
|
||||
assert result.recommendation_code == RECOMMENDATION_CODE_NO_CONFLICT
|
||||
|
||||
|
||||
def test_recommendation_token_changes_when_facts_change():
|
||||
vehicle = _vehicle("available")
|
||||
facts_a = _facts()
|
||||
facts_b = _facts(service_threshold_reached=True)
|
||||
assert compute_recommendation_token(vehicle, facts_a) != compute_recommendation_token(
|
||||
vehicle, facts_b
|
||||
)
|
||||
|
||||
|
||||
def test_recommendation_token_is_stable_for_identical_facts():
|
||||
vehicle = _vehicle("available")
|
||||
facts = _facts(active_booking_refs=["BK-0001"])
|
||||
assert compute_recommendation_token(vehicle, facts) == compute_recommendation_token(
|
||||
vehicle, facts
|
||||
)
|
||||
|
||||
|
||||
def test_recommendation_token_changes_when_vehicle_version_changes():
|
||||
facts = _facts()
|
||||
assert compute_recommendation_token(
|
||||
_vehicle("available", version=1), facts
|
||||
) != compute_recommendation_token(_vehicle("available", version=2), facts)
|
||||
Reference in New Issue
Block a user