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:
2026-09-14 07:50:26 +02:00
parent e3f946f7d4
commit 0e6a97ca61
133 changed files with 462 additions and 503 deletions
@@ -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 &lt;= b2 ET b1 &lt;= 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);
}