using System.Globalization; using Microsoft.AspNetCore.Mvc; using SchotzIadKmTracker.Api.Models.DTOs; using SchotzIadKmTracker.Api.Repositories; using SchotzIadKmTracker.Api.Services; using SchotzIadKmTracker.Api.Services.Exceptions; namespace SchotzIadKmTracker.Api.Controllers; /// /// Endpoints CRUD des Trajets, réordonnancement manuel et récupération des trajets d'une /// journée enrichis des coordonnées et géométries issues du Cache_Adresses (Exigences 1.3, /// 5.5, 5.6, 8.7, 9.3). /// [ApiController] [Route("api/trajets")] public class TrajetsController : ControllerBase { /// /// En-tête de réponse portant l'avertissement lorsqu'un Trajet a été enregistré sans /// distance calculable (Exigence 2.4). Le corps reste le attendu. /// private const string WarningHeader = "X-Trajet-Warning"; private readonly ITrajetService _trajetService; private readonly ITrajetRepository _trajetRepository; private readonly IAdresseCacheService _adresseCacheService; private readonly IPrecisionDistanceService _precisionDistanceService; public TrajetsController( ITrajetService trajetService, ITrajetRepository trajetRepository, IAdresseCacheService adresseCacheService, IPrecisionDistanceService precisionDistanceService) { _trajetService = trajetService; _trajetRepository = trajetRepository; _adresseCacheService = adresseCacheService; _precisionDistanceService = precisionDistanceService; } /// /// Crée un Trajet (Exigence 1.3). Retourne 201 + . Si la distance n'a /// pas pu être calculée faute de coordonnées, la réponse reste 201 et un avertissement est /// ajouté dans l'en-tête X-Trajet-Warning (Exigence 2.4). /// [HttpPost] [ProducesResponseType(typeof(TrajetDto), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Create( [FromBody] CreateTrajetDto dto, CancellationToken cancellationToken) { try { var resultat = await _trajetService.CreateAsync(dto, cancellationToken); if (resultat.Avertissement && !string.IsNullOrWhiteSpace(resultat.Message)) { Response.Headers[WarningHeader] = resultat.Message; } return CreatedAtAction( nameof(GetById), new { id = resultat.Trajet.Id }, resultat.Trajet); } catch (VehiculeNotFoundException ex) { // Aucun véhicule applicable à la date et aucun VehiculeId fourni. return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest); } } /// /// Recalcule la distance de TOUS les trajets via OpenRouteService (BAN + ORS), indépendamment /// d'un changement d'adresse. Sert à corriger des distances calculées en mode dégradé (ex. clé /// ORS invalide → distances à vol d'oiseau) une fois ORS de nouveau opérationnel. /// Retourne 200 + le nombre de trajets recalculés. /// [HttpPost("recalculer-distances")] [ProducesResponseType(typeof(RecalculDistancesResultDto), StatusCodes.Status200OK)] public async Task> RecalculerDistances( CancellationToken cancellationToken) { var nombre = await _trajetService.RecalculerToutesLesDistancesAsync(cancellationToken); return Ok(new RecalculDistancesResultDto(nombre)); } /// Récupère un Trajet par identifiant. 404 s'il est introuvable. [HttpGet("{id:int}")] [ProducesResponseType(typeof(TrajetDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetById(int id, CancellationToken cancellationToken) { var trajet = await _trajetRepository.GetByIdAsync(id, cancellationToken); if (trajet is null) { return Problem( detail: $"Le trajet {id} n'existe pas.", statusCode: StatusCodes.Status404NotFound); } return Ok(new TrajetDto( trajet.Id, trajet.Date, trajet.AdresseDepart, trajet.AdresseArrivee, // Distance EFFECTIVE (doublée si aller/retour) exposée au frontend. trajet.DistanceEffective, trajet.Commentaire, trajet.Position, trajet.VehiculeId, trajet.PuissanceFiscale, trajet.AllerRetour)); } /// /// Modifie un Trajet (Exigences 5.5, 5.6). Retourne 200 + . Un /// avertissement de distance non calculable est porté par l'en-tête X-Trajet-Warning. /// 404 si le Trajet est introuvable ; 503 si le recalcul de distance échoue (Exigence 5.8). /// [HttpPut("{id:int}")] [ProducesResponseType(typeof(TrajetDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] public async Task> Update( int id, [FromBody] UpdateTrajetDto dto, CancellationToken cancellationToken) { try { var resultat = await _trajetService.UpdateAsync(id, dto, cancellationToken); if (resultat.Avertissement && !string.IsNullOrWhiteSpace(resultat.Message)) { Response.Headers[WarningHeader] = resultat.Message; } return Ok(resultat.Trajet); } catch (TrajetNotFoundException ex) { return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound); } catch (VehiculeNotFoundException ex) { return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest); } catch (DistanceRecalculException ex) { // Échec du recalcul sur des coordonnées connues : modification annulée (rollback), // données existantes préservées (Exigence 5.8). Mappé sur 503 conformément à la table // d'erreurs de la conception (DistanceCalculationException → 503). return Problem(detail: ex.Message, statusCode: StatusCodes.Status503ServiceUnavailable); } } /// Supprime un Trajet (Exigence 5.5). 204 si succès, 404 s'il est introuvable (5.6). [HttpDelete("{id:int}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Delete(int id, CancellationToken cancellationToken) { try { await _trajetService.DeleteAsync(id, cancellationToken); return NoContent(); } catch (TrajetNotFoundException ex) { return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound); } } /// /// Réordonne manuellement les trajets d'une journée : met à jour uniquement leurs positions /// sans altérer les autres champs (Exigence 11.9). 204 en cas de succès. /// [HttpPatch("reordonner")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task Reordonner( [FromBody] ReordonnerTrajetsDto dto, CancellationToken cancellationToken) { await _trajetService.ReordonnerAsync(dto, cancellationToken); return NoContent(); } /// /// Retourne les trajets d'une journée enrichis des coordonnées et géométries connues via le /// Cache_Adresses (Exigences 8.7, 9.3). Le paramètre est attendu au /// format ISO yyyy-MM-dd pour la sûreté du routage. Les coordonnées/géométries absentes /// du cache restent à null. /// [HttpGet("journee/{date}")] [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task>> GetJournee( string date, CancellationToken cancellationToken) { if (!DateOnly.TryParseExact(date, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var jour)) { return Problem( detail: $"La date '{date}' est invalide. Format attendu : yyyy-MM-dd.", statusCode: StatusCodes.Status400BadRequest); } var trajets = await _trajetRepository.GetByDateAsync(jour, cancellationToken); // Précision courante (0/1/2 décimales) : lue une fois par appel, cohérente avec // ResumeService/TotauxService (Exigences 9.1, 9.4). var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken); var resultats = new List(trajets.Count); foreach (var trajet in trajets) { var depart = await _adresseCacheService.ObtenirParAdresseAsync( trajet.AdresseDepart, cancellationToken); var arrivee = await _adresseCacheService.ObtenirParAdresseAsync( trajet.AdresseArrivee, cancellationToken); // Géométrie de l'itinéraire : portée par le Trajet lui-même (paire départ→arrivée), // seule source correcte. Fallback sur le cache d'adresses pour d'éventuels trajets // hérités dont la géométrie n'a pas encore été recalculée (rétrocompatibilité). var geometrie = trajet.GeometrieJson ?? depart?.GeometrieJson ?? arrivee?.GeometrieJson; // Distance EFFECTIVE ARRONDIE : distance simple arrondie à la précision courante, // PUIS doublée si aller/retour (Exigence 4.2 — arrondi avant doublement), cohérente // avec ResumeService/TotauxService (Exigences 9.1, 9.4). null → 0 (comportement // inchangé). var distanceEffectiveArrondie = _precisionDistanceService.ArrondirDistance(trajet.Distance ?? 0m, precision) * (trajet.AllerRetour ? 2m : 1m); resultats.Add(new TrajetCarteDto( trajet.Id, trajet.Date, trajet.AdresseDepart, trajet.AdresseArrivee, distanceEffectiveArrondie, depart?.Latitude, depart?.Longitude, arrivee?.Latitude, arrivee?.Longitude, geometrie, trajet.Commentaire, trajet.AllerRetour)); } return Ok(resultats); } }