using Microsoft.EntityFrameworkCore; using SchotzIadKmTracker.Api.Data; using SchotzIadKmTracker.Api.Models.DTOs; using SchotzIadKmTracker.Api.Models.Entities; using SchotzIadKmTracker.Api.Services.Exceptions; namespace SchotzIadKmTracker.Api.Services; /// public class ResumeService : IResumeService { private const int MoisMin = 1; private const int MoisMax = 12; private const int AnneeMin = 2000; private const int AnneeMax = 2100; private readonly AppDbContext _context; private readonly IPrecisionDistanceService _precisionDistanceService; private readonly IAdresseCacheService _adresseCacheService; public ResumeService( AppDbContext context, IPrecisionDistanceService precisionDistanceService, IAdresseCacheService adresseCacheService) { _context = context; _precisionDistanceService = precisionDistanceService; _adresseCacheService = adresseCacheService; } /// public async Task GetResumeMensuelAsync( int annee, int mois, int? vehiculeId = null, CancellationToken cancellationToken = default) { ValiderAnnee(annee); ValiderMois(mois); var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken); var trajets = await ChargerTrajetsDuMoisAsync(annee, mois, vehiculeId, cancellationToken); // Coordonnées incluses : ce résumé alimente l'écran de consultation/saisie des Trajets, // dont l'insertion (« Insérer avant/après ») réutilise les coordonnées déjà géocodées // pour éviter un re-géocodage BAN. return await ConstruireResumeMensuelAvecCoordonneesAsync(annee, mois, trajets, precision, cancellationToken); } /// public async Task GetResumeAnnuelAsync( int annee, int? vehiculeId = null, CancellationToken cancellationToken = default) { ValiderAnnee(annee); var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken); // Charge une seule fois tous les trajets de l'année, puis répartit par mois. var query = _context.Trajets .AsNoTracking() .Where(t => t.Date.Year == annee); if (vehiculeId.HasValue) { query = query.Where(t => t.VehiculeId == vehiculeId.Value); } var trajetsAnnee = await query.ToListAsync(cancellationToken); var resumesMensuels = new List(MoisMax); for (var mois = MoisMin; mois <= MoisMax; mois++) { var trajetsDuMois = trajetsAnnee .Where(t => t.Date.Month == mois) .ToList(); resumesMensuels.Add(ConstruireResumeMensuel(annee, mois, trajetsDuMois, precision)); } // Total annuel = somme des 12 totaux mensuels, déjà arrondis (Exigences 4.3, 4.7, 6.6, 5.2, 5.3). var totalAnnuel = resumesMensuels.Sum(r => r.TotalKm); return new ResumeAnnuelDto(annee, resumesMensuels, totalAnnuel); } /// /// Charge les Trajets d'un mois donné, filtrés éventuellement par véhicule, triés par /// date croissante puis par Position croissante (Exigence 3.2). /// private async Task> ChargerTrajetsDuMoisAsync( int annee, int mois, int? vehiculeId, CancellationToken cancellationToken) { var query = _context.Trajets .AsNoTracking() .Where(t => t.Date.Year == annee && t.Date.Month == mois); if (vehiculeId.HasValue) { query = query.Where(t => t.VehiculeId == vehiculeId.Value); } // Tri par Position (decimal) non traduisible par SQLite : ordonnancement côté client. var trajets = await query.ToListAsync(cancellationToken); return trajets .OrderBy(t => t.Date) .ThenBy(t => t.Position) .ToList(); } /// /// Construit un à partir d'une liste de Trajets déjà triée. /// Chaque distance est arrondie (Precision_Distance) AVANT doublement aller/retour ; le total /// est la somme de ces valeurs déjà arrondies, sans second arrondi (Property 1, Property 2, Property 3). /// private ResumeMensuelDto ConstruireResumeMensuel( int annee, int mois, IReadOnlyList trajets, int precision) { // Calculée une seule fois par trajet, puis réutilisée pour le DTO et la somme du total // afin de garantir la même valeur (Distance_Effective_Arrondie) aux deux endroits. var distancesArrondies = trajets.Select(t => (t, distance: DistanceEffectiveArrondie(t, precision))).ToList(); var trajetsDto = distancesArrondies.Select(x => ToDto(x.t, x.distance)).ToList(); // Total = somme des Distance_Effective_Arrondie déjà calculées par trajet, null → 0 (Property 1, Property 3). var totalKm = distancesArrondies.Sum(x => x.distance ?? 0m); return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm); } /// /// Variante de enrichissant chaque /// des coordonnées de ses adresses, résolues via le Cache_Adresses (même mécanisme que /// TrajetsController.GetJournee). Réservée au résumé MENSUEL (écran de consultation/ /// saisie des Trajets, dont l'insertion réutilise ces coordonnées) : le résumé ANNUEL continue /// d'utiliser sans lookup, les totaux annuels n'ayant pas /// besoin du détail des coordonnées de chaque trajet (évite 2×N lookups cache inutiles). /// private async Task ConstruireResumeMensuelAvecCoordonneesAsync( int annee, int mois, IReadOnlyList trajets, int precision, CancellationToken cancellationToken) { var distancesArrondies = trajets.Select(t => (t, distance: DistanceEffectiveArrondie(t, precision))).ToList(); var trajetsDto = new List(distancesArrondies.Count); foreach (var (t, distance) in distancesArrondies) { var depart = await _adresseCacheService.ObtenirParAdresseAsync(t.AdresseDepart, cancellationToken); var arrivee = await _adresseCacheService.ObtenirParAdresseAsync(t.AdresseArrivee, cancellationToken); trajetsDto.Add(ToDto(t, distance, depart?.Latitude, depart?.Longitude, arrivee?.Latitude, arrivee?.Longitude)); } var totalKm = distancesArrondies.Sum(x => x.distance ?? 0m); return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm); } /// /// Calcule la Distance_Effective_Arrondie d'un trajet : arrondi (Precision_Distance) appliqué /// à la distance SIMPLE brute AVANT doublement aller/retour (Exigence 4.1, 4.2, 4.3). null si la /// distance brute n'a pas pu être calculée (Exigence 2.4 du résumé existant). /// private decimal? DistanceEffectiveArrondie(Trajet t, int precision) { if (!t.Distance.HasValue) { return null; } var distanceArrondie = _precisionDistanceService.ArrondirDistance(t.Distance.Value, precision); return t.AllerRetour ? distanceArrondie * 2m : distanceArrondie; } private void ValiderMois(int mois) { if (mois < MoisMin || mois > MoisMax) { throw new ResumeParametreInvalideException( $"Le mois doit être compris entre {MoisMin} et {MoisMax} inclus ; valeur reçue : {mois}."); } } private void ValiderAnnee(int annee) { if (annee < AnneeMin || annee > AnneeMax) { throw new ResumeParametreInvalideException( $"L'année doit être comprise entre {AnneeMin} et {AnneeMax} inclus ; valeur reçue : {annee}."); } } private static TrajetDto ToDto( Trajet t, decimal? distanceEffectiveArrondie, double? departLatitude = null, double? departLongitude = null, double? arriveeLatitude = null, double? arriveeLongitude = null) => new( t.Id, t.Date, t.AdresseDepart, t.AdresseArrivee, // Distance_Effective_Arrondie (arrondi Precision_Distance puis doublement si aller/retour), // alignée sur le TrajetDto commun (au lieu de DistanceEffective brut). distanceEffectiveArrondie, t.Commentaire, t.Position, t.VehiculeId, t.PuissanceFiscale, t.AllerRetour, departLatitude, departLongitude, arriveeLatitude, arriveeLongitude); }