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 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);
}