using Microsoft.EntityFrameworkCore; using TrajetKmTracker.Api.Data; using TrajetKmTracker.Api.Models.DTOs; using TrajetKmTracker.Api.Models.Entities; using TrajetKmTracker.Api.Services.Exceptions; namespace TrajetKmTracker.Api.Services; /// public class BaremeService : IBaremeService { private readonly AppDbContext _context; public BaremeService(AppDbContext context) { _context = context; } /// public async Task> GetBaremeAsync( int annee, CancellationToken cancellationToken = default) { // KmMin est un decimal : SQLite ne peut pas l'utiliser dans un ORDER BY, on trie côté client. var tranches = (await _context.TranchesBareme .AsNoTracking() .Where(t => t.Annee == annee) .ToListAsync(cancellationToken)) .OrderBy(t => t.PuissanceFiscale) .ThenBy(t => t.KmMin) .ToList(); return tranches.Select(ToDto).ToList(); } /// public async Task AddTrancheAsync( SaveTrancheBaremeDto dto, CancellationToken cancellationToken = default) { await EnsureNoOverlapAsync(dto.Annee, dto.PuissanceFiscale, dto.KmMin, dto.KmMax, excludeId: null, cancellationToken); var tranche = new TrancheBareme { Annee = dto.Annee, PuissanceFiscale = dto.PuissanceFiscale, KmMin = dto.KmMin, KmMax = dto.KmMax, Coefficient = dto.Coefficient, Base = dto.Base, }; _context.TranchesBareme.Add(tranche); await _context.SaveChangesAsync(cancellationToken); return ToDto(tranche); } /// public async Task UpdateTrancheAsync( int id, SaveTrancheBaremeDto dto, CancellationToken cancellationToken = default) { var tranche = await _context.TranchesBareme .FirstOrDefaultAsync(t => t.Id == id, cancellationToken) ?? throw new TrancheBaremeNotFoundException($"La tranche de barème d'identifiant {id} est introuvable."); await EnsureNoOverlapAsync(dto.Annee, dto.PuissanceFiscale, dto.KmMin, dto.KmMax, excludeId: id, cancellationToken); tranche.Annee = dto.Annee; tranche.PuissanceFiscale = dto.PuissanceFiscale; tranche.KmMin = dto.KmMin; tranche.KmMax = dto.KmMax; tranche.Coefficient = dto.Coefficient; tranche.Base = dto.Base; await _context.SaveChangesAsync(cancellationToken); return ToDto(tranche); } /// public async Task DeleteTrancheAsync(int id, CancellationToken cancellationToken = default) { var tranche = await _context.TranchesBareme .FirstOrDefaultAsync(t => t.Id == id, cancellationToken) ?? throw new TrancheBaremeNotFoundException($"La tranche de barème d'identifiant {id} est introuvable."); _context.TranchesBareme.Remove(tranche); await _context.SaveChangesAsync(cancellationToken); } /// public async Task CalculerMontantAsync( int annee, int puissanceFiscale, decimal totalKm, CancellationToken cancellationToken = default) { var tranches = await _context.TranchesBareme .AsNoTracking() .Where(t => t.Annee == annee && t.PuissanceFiscale == puissanceFiscale) .ToListAsync(cancellationToken); // Aucune tranche configurée : montant zéro + signal (Exigence 13.4), sans erreur bloquante. if (tranches.Count == 0) { return new BaremeCalculResult( Montant: 0m, BaremeConfigure: false, Coefficient: 0m, Base: 0m, Avertissement: $"Aucun barème configuré pour la puissance fiscale {puissanceFiscale} CV et l'année {annee} ; le montant de remboursement est nul."); } // Sélection de la tranche dont [KmMin, KmMax[ contient le total annuel (Property 24/27). // Borne max EXCLUSIVE ; KmMax == null = illimité. Les tranches ne se chevauchant pas, // au plus une est sélectionnable. var tranche = tranches.FirstOrDefault(t => totalKm >= t.KmMin && (t.KmMax == null || totalKm < t.KmMax.Value)); if (tranche is null) { // Des tranches existent mais aucune ne couvre ce total : traité comme non couvert (montant nul). return new BaremeCalculResult( Montant: 0m, BaremeConfigure: false, Coefficient: 0m, Base: 0m, Avertissement: $"Aucune tranche du barème ne couvre un total de {totalKm} km pour la puissance fiscale {puissanceFiscale} CV et l'année {annee} ; le montant de remboursement est nul."); } var montant = totalKm * tranche.Coefficient + tranche.Base; return new BaremeCalculResult( Montant: montant, BaremeConfigure: true, Coefficient: tranche.Coefficient, Base: tranche.Base, Avertissement: null); } /// public async Task CopierDernierBaremeAsync(int anneeCible, CancellationToken cancellationToken = default) { // Année source = année la plus récente STRICTEMENT antérieure à l'année cible qui a un barème. var anneesAvecBareme = (await _context.TranchesBareme .AsNoTracking() .Select(t => t.Annee) .Distinct() .ToListAsync(cancellationToken)) .Where(a => a < anneeCible) .OrderByDescending(a => a) .ToList(); if (anneesAvecBareme.Count == 0) { return 0; // Aucun barème antérieur : rien à recopier. } var anneeSource = anneesAvecBareme.First(); var source = await _context.TranchesBareme .AsNoTracking() .Where(t => t.Annee == anneeSource) .ToListAsync(cancellationToken); await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken); try { // Annule et remplace : on supprime d'abord les tranches existantes de l'année cible. var existantes = await _context.TranchesBareme .Where(t => t.Annee == anneeCible) .ToListAsync(cancellationToken); if (existantes.Count > 0) { _context.TranchesBareme.RemoveRange(existantes); await _context.SaveChangesAsync(cancellationToken); } // Copie des tranches de l'année source vers l'année cible. var copies = source.Select(t => new TrancheBareme { Annee = anneeCible, PuissanceFiscale = t.PuissanceFiscale, KmMin = t.KmMin, KmMax = t.KmMax, Coefficient = t.Coefficient, Base = t.Base, }).ToList(); _context.TranchesBareme.AddRange(copies); await _context.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); return copies.Count; } catch { await transaction.RollbackAsync(cancellationToken); throw; } } /// /// Vérifie qu'aucune tranche existante pour la même (année, puissance fiscale) ne chevauche /// l'intervalle [, ]. La tranche d'identifiant /// (en cours de mise à jour) est exclue de la comparaison. /// Deux intervalles [a1,a2] et [b1,b2] se chevauchent ssi a1 <= b2 ET b1 <= a2. /// private async Task EnsureNoOverlapAsync( int annee, int puissanceFiscale, decimal kmMin, decimal? kmMax, int? excludeId, CancellationToken cancellationToken) { var existantes = await _context.TranchesBareme .AsNoTracking() .Where(t => t.Annee == annee && t.PuissanceFiscale == puissanceFiscale && (excludeId == null || t.Id != excludeId.Value)) .ToListAsync(cancellationToken); // Convention [KmMin, KmMax[ (borne max EXCLUSIVE). KmMax == null = illimité (+infini). // Deux intervalles [a1,a2[ et [b1,b2[ se chevauchent ssi a1 < b2 ET b1 < a2, // en traitant une borne max nulle comme +infini. var chevauche = existantes.Any(t => { var aMaxInfini = kmMax == null; var bMaxInfini = t.KmMax == null; var condA = bMaxInfini || kmMin < t.KmMax!.Value; // a1 < b2 var condB = aMaxInfini || t.KmMin < kmMax!.Value; // b1 < a2 return condA && condB; }); if (chevauche) { throw new TranchesBaremeChevauchantesException( "Les tranches du barème pour cette puissance fiscale et cette année se chevauchent."); } } /// public async Task> GetAnneesAvecBaremeAsync(CancellationToken cancellationToken = default) { var annees = await _context.TranchesBareme .AsNoTracking() .Select(t => t.Annee) .Distinct() .ToListAsync(cancellationToken); annees.Sort(); return annees; } private static TrancheBaremeDto ToDto(TrancheBareme t) => new(t.Id, t.Annee, t.PuissanceFiscale, t.KmMin, t.KmMax, t.Coefficient, t.Base); }