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,141 @@
using System.Globalization;
using Microsoft.EntityFrameworkCore;
using SchotzIadKmTracker.Api.Data;
using SchotzIadKmTracker.Api.Models.DTOs;
using SchotzIadKmTracker.Api.Services.Exceptions;
namespace SchotzIadKmTracker.Api.Services;
/// <inheritdoc />
public class RemboursementService : IRemboursementService
{
private readonly AppDbContext _context;
private readonly IBaremeService _baremeService;
private readonly IMontantRegleService _montantRegleService;
public RemboursementService(
AppDbContext context,
IBaremeService baremeService,
IMontantRegleService montantRegleService)
{
_context = context;
_baremeService = baremeService;
_montantRegleService = montantRegleService;
}
/// <inheritdoc />
public async Task<RegularisationAnnuelleDto> 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.");
// 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)
.Select(t => new { t.Date.Month, t.Distance })
.ToListAsync(cancellationToken);
var kmMoisMap = trajetsAnnee
.GroupBy(t => t.Month)
.ToDictionary(g => g.Key, g => g.Sum(t => t.Distance ?? 0m));
// 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<LigneSyntheseMoisDto>(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: Math.Round(kmMois, 2, MidpointRounding.AwayFromZero),
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: Math.Round(totalKmAnnuel, 2, MidpointRounding.AwayFromZero),
TotalPaye: totalPaye,
MontantReel: montantReel,
Regularisation: regularisation);
}
/// <summary>
/// Construit un libellé lisible de la tranche annuelle dont l'intervalle contient
/// <paramref name="totalKmAnnuel"/>. Retourne un libellé « non configuré » si aucune tranche
/// ne s'applique (barème absent ou total hors des intervalles définis).
/// </summary>
private async Task<string> 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);
}
}