Renommage : TrajetKmTracker -> SchotzIadKmTracker (backend, specs, frontend)
- Backend : projets, solution, namespaces et références TrajetKmTracker.Api/.Tests -> SchotzIadKmTracker.Api/.Tests (dossiers, .csproj, .sln, .http, namespaces C#). GUID UserSecretsId préservé. - Specs : dossier .kiro/specs/trajet-km-tracker -> schotziad-km-tracker et mise à jour des références internes (chemins, tags de propriétés). - Frontend : projet Angular trajet-km-tracker -> schotziad-km-tracker (package.json, angular.json, chemin dist, Dockerfile), titre applicatif 'SchotzIAD KM Tracker'. - Docker et scripts (backend/Dockerfile, frontend/Dockerfile, run-dev.sh) mis à jour. - Suppression du fichier résiduel package.json.bak. Backend (solution) et frontend (build production) compilent sans erreur.
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
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;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class BaremeService : IBaremeService
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
|
||||
public BaremeService(AppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TrancheBaremeDto>> 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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TrancheBaremeDto> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TrancheBaremeDto> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BaremeCalculResult> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vérifie qu'aucune tranche existante pour la même (année, puissance fiscale) ne chevauche
|
||||
/// l'intervalle [<paramref name="kmMin"/>, <paramref name="kmMax"/>]. La tranche d'identifiant
|
||||
/// <paramref name="excludeId"/> (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.
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<int>> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user