using System.Globalization; using Microsoft.EntityFrameworkCore; using SchotzIadKmTracker.Api.Data; using SchotzIadKmTracker.Api.Models.DTOs; using SchotzIadKmTracker.Api.Services.Exceptions; namespace SchotzIadKmTracker.Api.Services; /// public class RemboursementService : IRemboursementService { private readonly AppDbContext _context; private readonly IBaremeService _baremeService; private readonly IMontantRegleService _montantRegleService; private readonly IPrecisionDistanceService _precisionDistanceService; public RemboursementService( AppDbContext context, IBaremeService baremeService, IMontantRegleService montantRegleService, IPrecisionDistanceService precisionDistanceService) { _context = context; _baremeService = baremeService; _montantRegleService = montantRegleService; _precisionDistanceService = precisionDistanceService; } /// public async Task GetRegularisationAnnuelleAsync( int annee, int vehiculeId, CancellationToken cancellationToken = default) { var vehicule = await _context.Vehicules .AsNoTracking() .FirstOrDefaultAsync(v => v.Id == vehiculeId, cancellationToken) ?? throw new VehiculeNotFoundException($"Le véhicule d'identifiant {vehiculeId} est introuvable."); // Précision d'arrondi courante (Exigence 4.1) : lue une fois pour toute la requête. var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken); // Kilomètres du véhicule par mois pour l'année (Distance nullable → 0 quand null). // Le GROUP BY + SUM sur decimal n'est pas traduisible par SQLite : on ramène les // trajets de l'année puis on agrège côté client. var trajetsAnnee = await _context.Trajets .AsNoTracking() .Where(t => t.VehiculeId == vehiculeId && t.Date.Year == annee) // On ramène la distance simple et l'indicateur aller/retour, puis on agrège la // distance EFFECTIVE (arrondie AVANT doublement si A/R — Exigence 4.2) côté client. .Select(t => new { t.Date.Month, t.Distance, t.AllerRetour }) .ToListAsync(cancellationToken); var kmMoisMap = trajetsAnnee .GroupBy(t => t.Month) .ToDictionary( g => g.Key, g => g.Sum(t => _precisionDistanceService.ArrondirDistance(t.Distance ?? 0m, precision) * (t.AllerRetour ? 2m : 1m))); // Total kilométrique annuel COMPLET (détermine la tranche — Exigence 14.5, Property 27). var totalKmAnnuel = kmMoisMap.Values.Sum(); // Barème plein (100%) appliqué au total annuel : Montant_Reel = total_km × coef + base. var bareme = await _baremeService.CalculerMontantAsync( annee, vehicule.PuissanceFiscale, totalKmAnnuel, cancellationToken); var coefficient = bareme.Coefficient; var baseTranche = bareme.Base; var montantReel = Math.Round(bareme.Montant, 2, MidpointRounding.AwayFromZero); // Libellé de la tranche annuelle applicable (ex. « 5 001 à 20 000 km »). var trancheLibelle = await BuildTrancheLibelleAsync( annee, vehicule.PuissanceFiscale, totalKmAnnuel, bareme.BaremeConfigure, cancellationToken); // Montant_Regle par mois pour ce véhicule/année. var montantsRegles = await _montantRegleService.GetMontantsReglesAsync(annee, vehiculeId, cancellationToken); var reglesMap = montantsRegles.ToDictionary(m => m.Mois, m => m.Montant); // 12 lignes mensuelles : // FraisReelMensuel = km_du_mois × coefficient de la tranche annuelle (Exigence 14.6), // FraisPaye = Montant_Regle saisi du mois (0 si absent). var lignes = new List(12); for (var mois = 1; mois <= 12; mois++) { var kmMois = kmMoisMap.TryGetValue(mois, out var km) ? km : 0m; var fraisPaye = reglesMap.TryGetValue(mois, out var paye) ? paye : 0m; var fraisReelMensuel = Math.Round(kmMois * coefficient, 2, MidpointRounding.AwayFromZero); lignes.Add(new LigneSyntheseMoisDto( Mois: mois, KmMois: kmMois, FraisPaye: fraisPaye, FraisReelMensuel: fraisReelMensuel)); } var totalPaye = Math.Round(montantsRegles.Sum(m => m.Montant), 2, MidpointRounding.AwayFromZero); // Regularisation = Montant_Reel − total des Montant_Regle (Exigence 14.7). var regularisation = Math.Round(montantReel - totalPaye, 2, MidpointRounding.AwayFromZero); return new RegularisationAnnuelleDto( Annee: annee, VehiculeId: vehiculeId, Plaque: vehicule.Plaque, PuissanceFiscale: vehicule.PuissanceFiscale, CoefficientTranche: coefficient, BaseTranche: baseTranche, TrancheLibelle: trancheLibelle, Mois: lignes, TotalKmAnnuel: totalKmAnnuel, TotalPaye: totalPaye, MontantReel: montantReel, Regularisation: regularisation); } /// /// Construit un libellé lisible de la tranche annuelle dont l'intervalle contient /// . Retourne un libellé « non configuré » si aucune tranche /// ne s'applique (barème absent ou total hors des intervalles définis). /// private async Task BuildTrancheLibelleAsync( int annee, int puissanceFiscale, decimal totalKmAnnuel, bool baremeConfigure, CancellationToken cancellationToken) { if (!baremeConfigure) { return "Barème non configuré"; } var tranches = await _context.TranchesBareme .AsNoTracking() .Where(t => t.Annee == annee && t.PuissanceFiscale == puissanceFiscale) .ToListAsync(cancellationToken); var tranche = tranches .FirstOrDefault(t => totalKmAnnuel >= t.KmMin && (t.KmMax == null || totalKmAnnuel < t.KmMax.Value)); if (tranche is null) { return "Barème non configuré"; } return tranche.KmMax.HasValue ? $"{FormatKm(tranche.KmMin)} à {FormatKm(tranche.KmMax.Value)} km" : $"{FormatKm(tranche.KmMin)} km et plus"; } private static string FormatKm(decimal km) { // Espace comme séparateur de milliers, sans décimales (bornes de tranche entières). var fr = CultureInfo.GetCultureInfo("fr-FR"); return Math.Round(km, 0, MidpointRounding.AwayFromZero).ToString("N0", fr); } }