Files
SchotzIAD/backend/SchotzIadKmTracker.Api/Services/RemboursementService.cs
T
MadJax 09c24d5f59 UX consultation, trajets aller/retour et rafraîchissement des totaux
- Totaux : rafraîchissement immédiat du panneau (jour/mois/année + montant
  prévisionnel) après création/modification/suppression/réordonnancement d'un
  trajet, sans changer de jour (ViewChild + méthode publique rafraichir()).
- Trajets Aller/Retour : case à cocher au formulaire ; la distance comptée est
  doublée (distance simple stockée + booléen AllerRetour, distance effective
  utilisée dans tous les agrégats : totaux, résumés, barème, régularisation, PDF).
  Migration TrajetAllerRetour. Badge « A/R » dans une colonne dédiée en début de
  ligne pour garder date/adresses alignées.
- Mise en page de la page Trajets : vue à hauteur fixe où seule la liste défile ;
  colonne gauche figée (sélecteur de date + navigation, totaux, carte) et bande
  fixe (véhicule + bouton d'ajout) au-dessus de la liste. Layout applicatif global
  en pleine hauteur (app.component).
- run-dev.sh : chargement de la clé JWT depuis .env (ou secret de dev aléatoire) et
  Auth__FrontendBaseUrl sur le frontend dev ; rappel du lien de reset journalisé.

Build backend (solution) et frontend (production) OK.
2026-09-15 12:33:10 +02:00

144 lines
6.2 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 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)
// On ramène la distance simple et l'indicateur aller/retour, puis on agrège la
// distance EFFECTIVE (doublée si A/R) côté client.
.Select(t => new { t.Date.Month, t.Distance, t.AllerRetour })
.ToListAsync(cancellationToken);
var kmMoisMap = trajetsAnnee
.GroupBy(t => t.Month)
.ToDictionary(g => g.Key, g => g.Sum(t => (t.Distance ?? 0m) * (t.AllerRetour ? 2m : 1m)));
// 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);
}
}