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; public ResumeService(AppDbContext context) { _context = context; } /// public async Task GetResumeMensuelAsync( int annee, int mois, int? vehiculeId = null, CancellationToken cancellationToken = default) { ValiderAnnee(annee); ValiderMois(mois); var trajets = await ChargerTrajetsDuMoisAsync(annee, mois, vehiculeId, cancellationToken); return ConstruireResumeMensuel(annee, mois, trajets); } /// public async Task GetResumeAnnuelAsync( int annee, int? vehiculeId = null, CancellationToken cancellationToken = default) { ValiderAnnee(annee); // 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)); } // Total annuel = somme des 12 totaux mensuels (Exigences 4.3, 4.7, 6.6). var totalAnnuel = Arrondir(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. /// Le total est la somme des distances (null → 0) arrondie à 2 décimales (Property 1). /// private static ResumeMensuelDto ConstruireResumeMensuel(int annee, int mois, IReadOnlyList trajets) { var trajetsDto = trajets.Select(ToDto).ToList(); // Total = somme des distances EFFECTIVES (doublées si aller/retour), null → 0 (Property 1). var totalKm = Arrondir(trajets.Sum(t => t.DistanceEffective ?? 0m)); return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm); } 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 decimal Arrondir(decimal valeur) => Math.Round(valeur, 2, MidpointRounding.AwayFromZero); private static TrajetDto ToDto(Trajet t) => new( t.Id, t.Date, t.AdresseDepart, t.AdresseArrivee, // Distance EFFECTIVE (doublée si aller/retour), alignée sur le TrajetDto commun. t.DistanceEffective, t.Commentaire, t.Position, t.VehiculeId, t.PuissanceFiscale, t.AllerRetour); }