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
+117 -5
View File
@@ -249,28 +249,74 @@ def test_resolve_overlap_blocks_one_booking_and_resolves(ops_client):
assert booking["status"] == "blocked"
def test_apply_recommended_status_requires_operations_manager(employee_client):
def test_status_recommendation_requires_operations_manager(employee_client):
response = employee_client.post(
"/api/v1/data-quality/issues/DQ-DEMO-STATUS/apply-recommended-status"
"/api/v1/data-quality/issues/DQ-DEMO-STATUS/status-recommendation"
)
assert response.status_code == 403
def test_apply_recommended_status_requires_operations_manager(employee_client):
response = employee_client.post(
"/api/v1/data-quality/issues/DQ-DEMO-STATUS/apply-recommended-status",
json={"recommendation_token": "irrelevant"},
)
assert response.status_code == 403
def test_status_recommendation_preview_does_not_mutate_anything(ops_client):
target = _first_open(ops_client, "vehicle_status_conflict")
vehicle_before = ops_client.get(f"/api/v1/vehicles/{target['entity_ref']}").json()
preview_response = ops_client.post(
f"/api/v1/data-quality/issues/{target['public_ref']}/status-recommendation"
)
assert preview_response.status_code == 200
preview = preview_response.json()
assert preview["current_status"] == vehicle_before["operational_status"]
assert preview["recommendation_token"]
assert "facts" in preview
# Calling preview again (as the UI would on every open) must still not mutate.
ops_client.post(f"/api/v1/data-quality/issues/{target['public_ref']}/status-recommendation")
issue_after = ops_client.get(f"/api/v1/data-quality/issues/{target['public_ref']}").json()
vehicle_after = ops_client.get(f"/api/v1/vehicles/{target['entity_ref']}").json()
assert issue_after["status"] == "open"
assert vehicle_after["operational_status"] == vehicle_before["operational_status"]
def test_apply_recommended_status_resolves_conflict(ops_client):
target = _first_open(ops_client, "vehicle_status_conflict")
preview = ops_client.post(
f"/api/v1/data-quality/issues/{target['public_ref']}/status-recommendation"
).json()
assert preview["safe_to_apply"] is True
assert preview["manual_review_required"] is False
response = ops_client.post(
f"/api/v1/data-quality/issues/{target['public_ref']}/apply-recommended-status"
f"/api/v1/data-quality/issues/{target['public_ref']}/apply-recommended-status",
json={"recommendation_token": preview["recommendation_token"]},
)
assert response.status_code == 200
body = response.json()
assert body["issue"]["status"] == "resolved"
assert body["applied_status"]
assert body["reason"]
assert body["applied_status"] == preview["recommended_status"]
assert body["reason_code"] == preview["recommendation_code"]
vehicle = ops_client.get(f"/api/v1/vehicles/{target['entity_ref']}").json()
assert vehicle["operational_status"] == body["applied_status"]
def test_apply_recommended_status_rejects_stale_token(ops_client):
target = _first_open(ops_client, "vehicle_status_conflict")
response = ops_client.post(
f"/api/v1/data-quality/issues/{target['public_ref']}/apply-recommended-status",
json={"recommendation_token": "not-a-real-token"},
)
assert response.status_code == 409
assert response.json()["error"]["code"] == "RECOMMENDATION_STALE"
def test_resolve_odometer_regression_requires_operations_manager(employee_client):
response = employee_client.post(
"/api/v1/data-quality/issues/DQ-0007/resolve-odometer-regression",
@@ -365,6 +411,72 @@ def test_manual_scan_records_audit_event(ops_client):
assert "created" in events[0]["metadata"]
def _reset_demo(ops_client) -> None:
# /api/v1/demo/reset deletes the session cookie (the reset recreates the users
# table, so the old session's user id no longer exists) -- the caller must log back
# in before making any further authenticated call with the same client.
response = ops_client.post("/api/v1/demo/reset")
assert response.status_code == 200, response.text
login_response = ops_client.post(
"/api/v1/demo/login", json={"role": "operations_manager"}
)
assert login_response.status_code == 200, login_response.text
def _resolve_overlap_issue(ops_client, *, booking_to_block: str) -> None:
overlap = _first_open(ops_client, "booking_overlap")
response = ops_client.post(
f"/api/v1/data-quality/issues/{overlap['public_ref']}/resolve-overlap",
json={"booking_ref": booking_to_block},
)
assert response.status_code == 200, response.text
assert response.json()["status"] == "resolved"
def _apply_status_recommendation(ops_client, public_ref: str) -> dict:
preview = ops_client.post(
f"/api/v1/data-quality/issues/{public_ref}/status-recommendation"
).json()
apply_response = ops_client.post(
f"/api/v1/data-quality/issues/{public_ref}/apply-recommended-status",
json={"recommendation_token": preview["recommendation_token"]},
)
assert apply_response.status_code == 200, apply_response.text
return apply_response.json()
def test_mo_016_status_conflict_recommendation_is_order_independent(ops_client):
# MO-016 carries both a booking_overlap (DQ-DEMO-OVERLAP) and a vehicle_status_conflict
# (DQ-DEMO-STATUS) issue at once -- resolving them in either order must land the
# vehicle in the same final, safe state (see docs/fleet-ops-correction/
# current-gap-audit.md §6-7 and vehicle-status-decision-table.md).
# Order A: resolve the booking overlap first, then the status conflict.
_reset_demo(ops_client)
_resolve_overlap_issue(ops_client, booking_to_block="BK-DEMO-OVERLAP-B")
status_issue_a = _first_open(ops_client, "vehicle_status_conflict")
result_a = _apply_status_recommendation(ops_client, status_issue_a["public_ref"])
vehicle_a = ops_client.get("/api/v1/vehicles/MO-016").json()
# Order B: resolve the status conflict first, then the booking overlap.
_reset_demo(ops_client)
status_issue_b = _first_open(ops_client, "vehicle_status_conflict")
result_b = _apply_status_recommendation(ops_client, status_issue_b["public_ref"])
_resolve_overlap_issue(ops_client, booking_to_block="BK-DEMO-OVERLAP-B")
vehicle_b = ops_client.get("/api/v1/vehicles/MO-016").json()
assert result_a["applied_status"] == result_b["applied_status"], (
"resolving the booking overlap before vs. after the status conflict recommended "
"a different status -- the recommendation must not depend on issue order"
)
assert vehicle_a["operational_status"] == vehicle_b["operational_status"]
# Never auto-"available" and never auto-"rented" as a side effect of this scenario --
# MO-016 has no active rental in either order, only reserved bookings.
assert vehicle_a["operational_status"] not in ("rented",)
_reset_demo(ops_client)
def test_rejected_issue_recurrence_links_to_prior_decision(ops_client):
# Reject an open vehicle_status_conflict issue without changing the vehicle, so the
# next scan re-detects the same unresolved condition -- it must not silently vanish