Files
SchotzIAD/backend/TrajetKmTracker.Api/Services/RemboursementService.cs
T
MadJax e3f946f7d4 SchotzIAD KM Tracker : suivi des trajets kilométriques (Angular + .NET 8)
Application complète de saisie et de suivi des trajets kilométriques avec
calcul de distance routière (OpenRouteService), barème kilométrique, résumés
et régularisation annuelle.

Backend (ASP.NET Core .NET 8, EF Core, SQLite) :
- Trajets (CRUD, réordonnancement, insertion), calcul de distance ORS avec
  fallback Haversine, géométrie d'itinéraire stockée par trajet, recalcul en masse.
- Profil, véhicules (avec dénomination), carnet d'adresses nommées, cache d'adresses.
- Barème kilométrique : tranches multiples par puissance fiscale, borne max
  exclusive avec 'illimité' (KmMax nullable), coefficient à 3 décimales,
  recopie du dernier barème connu.
- Résumés mensuel/annuel, montants réglés, régularisation, export PDF.

Frontend (Angular 22, zoneless + signals) :
- Consultation par jour, navigation temporelle synchronisée, carte Leaflet,
  formulaire de trajet (adresses nommées, domicile, autocomplétion BAN).
- Détection de changement zoneless (indépendante de zone.js).

Hébergement : Docker Compose (nginx + backend), scripts run-dev.sh / run-docker.sh.
2026-09-14 02:08:27 +02:00

142 lines
6.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using Microsoft.EntityFrameworkCore;
using TrajetKmTracker.Api.Data;
using TrajetKmTracker.Api.Models.DTOs;
using TrajetKmTracker.Api.Services.Exceptions;
namespace TrajetKmTracker.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);
}
}