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();
var totalKm = Arrondir(trajets.Sum(t => t.Distance ?? 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,
t.Distance,
t.Commentaire,
t.Position,
t.VehiculeId,
t.PuissanceFiscale);
}