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; public ResumeService(AppDbContext context, IPrecisionDistanceService precisionDistanceService) { _context = context; _precisionDistanceService = precisionDistanceService; } /// 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); return ConstruireResumeMensuel(annee, mois, trajets, precision); } /// 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); } /// /// 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) => 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); }