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
+40 -5
View File
@@ -180,13 +180,48 @@
"heading": "Résoudre le conflit de statut",
"description": "Une règle faisant autorité recommande un statut opérationnel corrigé pour ce véhicule.",
"currentStatus": "Statut actuel",
"calculateAndApply": "Calculer et appliquer le statut recommandé",
"confirmTitle": "Confirmer le changement de statut",
"confirmBody": "Appliquer le statut recommandé faisant autorité pour ce véhicule ?",
"reviewRecommendation": "Voir la recommandation",
"loadingRecommendation": "Chargement de la recommandation…",
"recommendationFailed": "Impossible de charger une recommandation.",
"recommendedStatus": "Statut recommandé",
"whyHeading": "Pourquoi",
"evidenceHeading": "Faits",
"consequenceHeading": "Conséquence",
"consequence": {
"statusWillChange": "Le statut du véhicule sera changé en {{status}}.",
"issueWillBeRechecked": "Ce problème de qualité sera à nouveau contrôlé.",
"changeWillBeAudited": "Le changement sera enregistré dans la piste d'audit.",
"bookingsNotDeleted": "Les réservations elles-mêmes ne sont pas supprimées."
},
"changeStatusTo": "Changer le statut vers {{status}}",
"applying": "Application en cours…",
"confirmYes": "Oui, appliquer",
"applied": "Appliqué {{status}} — {{reason}}",
"applyFailed": "Impossible d'appliquer un statut recommandé."
"applyFailed": "Impossible d'appliquer un statut recommandé.",
"staleRecommendation": "La situation a changé depuis l'affichage de cette recommandation. Consultez-la à nouveau avant de l'appliquer.",
"reviewAgain": "Revoir la recommandation",
"manualReview": {
"heading": "Évaluation manuelle requise",
"body": "Les faits concernant ce véhicule se contredisent. Un Operations Manager doit évaluer la situation en personne plutôt que d'appliquer un changement de statut automatique.",
"hint": "Utilisez « Reporter » ou « Rejeter » ci-dessous, ou examinez manuellement le véhicule et les réservations concernées."
},
"noConflict": {
"heading": "Aucun changement nécessaire",
"body": "Le statut actuel de ce véhicule correspond déjà aux faits."
},
"evidence": {
"activeBookings": "Réservation(s) active(s) : {{refs}}",
"overlappingBookings": "Réservations qui se chevauchent : {{pairs}}",
"serviceThresholdReached": "Le kilométrage ({{odometer}} km) a atteint le seuil d'entretien ({{threshold}} km).",
"openOverlapIssue": "Problème de chevauchement de réservation ouvert : {{ref}}"
},
"reasonCodes": {
"vehicle.active_rental": "Ce véhicule a une location active mais n'est pas enregistré comme loué. Le statut est corrigé pour refléter la situation réelle.",
"vehicle.service_threshold_reached": "Ce véhicule a atteint son seuil d'entretien. Il est placé en entretien afin qu'il ne soit pas utilisé avant que la révision ait eu lieu.",
"vehicle.booking_conflict": "Ce véhicule a deux réservations qui se chevauchent. Le bloquer évite qu'il soit à nouveau considéré comme disponible avant que le conflit de réservation ne soit résolu.",
"vehicle.rental_ended": "La location de ce véhicule est terminée et il n'y a plus de réservation active. Il est remis en disponible.",
"vehicle.manual_review_required": "Les faits concernant ce véhicule se contredisent (par exemple, une réservation active alors que le véhicule est en entretien, ou un seuil d'entretien atteint en même temps qu'une location active). Cela nécessite une évaluation humaine plutôt qu'un changement de statut automatique.",
"vehicle.no_conflict": "Le statut actuel de ce véhicule correspond déjà aux faits. Aucun changement n'est nécessaire."
}
}
}
}