Precision configurable des distances (arrondi superieur 0/1/2 decimales)
- Nouveau parametre global Precision_Distance (sous-menu Application de la Page_Parametrage, entre Compte et Vehicules), persiste via une entite singleton ParametresApplication, expose par GET/PUT /api/parametres-application - Arrondi superieur (ceiling) applique a l'affichage et aux agregats : trajet simple, aller/retour (arrondi puis doublement), totaux jour/mois/annee, carte, export PDF et bareme kilometrique - jamais a la distance brute stockee - Refonte du Montant_Previsionnel : le montant mensuel (80%) porte desormais sur le seul mois affiche (tranche determinee par le cumul annuel) ; nouvelle ligne Montant_Previsionnel_Annuel (100%, sans minoration, sur le cumul) - Backend : PrecisionDistanceService (ArrondirDistance), integration dans ResumeService/TotauxService/RemboursementService/MontantRegleService, migration EF Core ; 7 tests de propriete (FsCheck) + tests existants, 62/62 verts - Frontend : ParametresApplicationService (signal partage), composant de parametrage, fonction miroir arrondirDistance, adaptation de la liste des trajets, du panneau des totaux, de la carte et des montants regles - Specs : nouvelle spec precision-distances-km (requirements/design/tasks) ; mise a jour de requirements.md, design.md et design-requirements.md de schotziad-km-tracker (Requirement 22, renvois croises, Montant_Previsionnel) - Suppression du fichier chromium .deb suivi par erreur dans le depot
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
namespace SchotzIadKmTracker.Api.Services.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Levée lorsqu'une valeur de Precision_Distance soumise n'est pas 0, 1 ou 2.
|
||||
/// Convertie en réponse HTTP 400 (Bad Request) — Exigence 1.6.
|
||||
/// </summary>
|
||||
public class PrecisionDistanceInvalideException : Exception
|
||||
{
|
||||
public PrecisionDistanceInvalideException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public PrecisionDistanceInvalideException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,13 @@ public interface IMontantRegleService
|
||||
|
||||
/// <summary>
|
||||
/// Calcule le Montant_Previsionnel indicatif pour un (année, mois, véhicule) :
|
||||
/// 80% du Bareme_Kilometrique plein appliqué au Total_Annuel_Partiel du véhicule
|
||||
/// (somme des distances de l'année pour ce véhicule jusqu'au mois indiqué inclus),
|
||||
/// arrondi à deux décimales (Property 23, Exigences 13.5/13.6).
|
||||
/// Cette valeur est purement informative et n'entre jamais dans la régularisation.
|
||||
/// la tranche de barème est déterminée par le Total_Annuel_Partiel du véhicule (somme des
|
||||
/// distances arrondies de l'année pour ce véhicule jusqu'au mois indiqué inclus — Exigence 7.2),
|
||||
/// puis son coefficient/base est appliqué au seul kilométrage arrondi du mois affiché pour
|
||||
/// obtenir le Montant_Previsionnel_Mensuel (80%, Exigences 6.1/6.2/7.1/7.4), et au cumul annuel
|
||||
/// pour obtenir le Montant_Previsionnel_Annuel (100%, Exigences 8.1/8.2/8.5), tous deux arrondis
|
||||
/// à deux décimales.
|
||||
/// Ces valeurs sont purement informatives et n'entrent jamais dans la régularisation.
|
||||
/// </summary>
|
||||
Task<MontantPrevisionnelDto> GetMontantPrevisionnelAsync(
|
||||
int annee, int mois, int vehiculeId, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace SchotzIadKmTracker.Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Lecture/écriture de la configuration globale Precision_Distance, et fonction utilitaire
|
||||
/// pure d'arrondi supérieur (ceiling) réutilisée par tous les points de calcul de distance.
|
||||
/// </summary>
|
||||
public interface IPrecisionDistanceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retourne la précision courante (0, 1 ou 2). Crée la ligne singleton avec la valeur
|
||||
/// par défaut (2) si elle est absente (Exigence 1.2), sur le même modèle que
|
||||
/// <see cref="IProfilService"/>.
|
||||
/// </summary>
|
||||
Task<int> ObtenirAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Définit la précision. Lève <see cref="Exceptions.PrecisionDistanceInvalideException"/>
|
||||
/// si la valeur n'est pas 0, 1 ou 2 (Exigence 1.6).
|
||||
/// </summary>
|
||||
Task<int> DefinirAsync(int valeur, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Arrondit une distance brute (km) au supérieur (ceiling) à la précision indiquée.
|
||||
/// Fonction pure, sans effet de bord, sans accès base — utilisable partout où une
|
||||
/// distance doit être affichée ou agrégée (Exigences 3.2–3.5, 4.2).
|
||||
/// Formule : Ceiling(distanceBrute × 10^precision) / 10^precision.
|
||||
/// </summary>
|
||||
decimal ArrondirDistance(decimal distanceBrute, int precision);
|
||||
}
|
||||
@@ -13,11 +13,14 @@ public class MontantRegleService : IMontantRegleService
|
||||
|
||||
private readonly AppDbContext _context;
|
||||
private readonly IBaremeService _baremeService;
|
||||
private readonly IPrecisionDistanceService _precisionDistanceService;
|
||||
|
||||
public MontantRegleService(AppDbContext context, IBaremeService baremeService)
|
||||
public MontantRegleService(
|
||||
AppDbContext context, IBaremeService baremeService, IPrecisionDistanceService precisionDistanceService)
|
||||
{
|
||||
_context = context;
|
||||
_baremeService = baremeService;
|
||||
_precisionDistanceService = precisionDistanceService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -85,37 +88,58 @@ public class MontantRegleService : IMontantRegleService
|
||||
.FirstOrDefaultAsync(v => v.Id == vehiculeId, cancellationToken)
|
||||
?? throw new VehiculeNotFoundException($"Le véhicule d'identifiant {vehiculeId} est introuvable.");
|
||||
|
||||
// Total_Annuel_Partiel : somme des distances de l'année pour ce véhicule,
|
||||
// jusqu'au mois sélectionné inclus (les mois postérieurs sont exclus).
|
||||
// SQLite ne sait pas appliquer SUM sur une colonne decimal : on ramène les distances
|
||||
// concernées et on somme côté client (null traité comme 0).
|
||||
var distancesPartielles = await _context.Trajets
|
||||
var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken);
|
||||
|
||||
// Trajets de l'année pour ce véhicule, jusqu'au mois sélectionné inclus (les mois
|
||||
// postérieurs sont exclus). SQLite ne sait pas appliquer SUM sur une colonne decimal :
|
||||
// on ramène les trajets concernés et on somme côté client (distance null traitée comme 0).
|
||||
// Le mois de chaque trajet est conservé pour distinguer le mois affiché du cumul annuel.
|
||||
var trajets = await _context.Trajets
|
||||
.AsNoTracking()
|
||||
.Where(t => t.VehiculeId == vehiculeId
|
||||
&& t.Date.Year == annee
|
||||
&& t.Date.Month <= mois)
|
||||
// On ramène la distance simple et l'indicateur aller/retour, puis on somme la
|
||||
// distance EFFECTIVE (doublée si A/R) côté client (null → 0).
|
||||
.Select(t => new { t.Distance, t.AllerRetour })
|
||||
.Select(t => new { t.Date.Month, t.Distance, t.AllerRetour })
|
||||
.ToListAsync(cancellationToken);
|
||||
var totalAnnuelPartiel = distancesPartielles.Sum(x => x.Distance.GetValueOrDefault() * (x.AllerRetour ? 2m : 1m));
|
||||
|
||||
// Barème plein appliqué au total partiel (sans minoration).
|
||||
var bareme = await _baremeService.CalculerMontantAsync(
|
||||
// Distance_Effective_Arrondie : arrondi de la distance simple à la précision courante,
|
||||
// PUIS doublement si aller/retour (arrondi avant doublement — Exigence 4.2).
|
||||
decimal DistanceArrondie(decimal? distance, bool allerRetour) =>
|
||||
_precisionDistanceService.ArrondirDistance(distance ?? 0m, precision) * (allerRetour ? 2m : 1m);
|
||||
|
||||
// Total_Mois_Km : somme des distances déjà arrondies des trajets du mois affiché (Exigence 7.1).
|
||||
var totalMoisAffiche = trajets
|
||||
.Where(t => t.Month == mois)
|
||||
.Sum(t => DistanceArrondie(t.Distance, t.AllerRetour));
|
||||
|
||||
// Total_Annuel_Partiel : somme des distances déjà arrondies de tous les trajets chargés
|
||||
// (cumul jusqu'au mois inclus — Exigence 6.2).
|
||||
var totalAnnuelPartiel = trajets.Sum(t => DistanceArrondie(t.Distance, t.AllerRetour));
|
||||
|
||||
// La tranche de barème est déterminée par le cumul annuel (Exigence 7.2), et son
|
||||
// coefficient/base est réutilisé tel quel pour le mois affiché (pas de nouvel appel à
|
||||
// CalculerMontantAsync avec le total du mois seul : cela re-sélectionnerait une tranche
|
||||
// différente selon le mois seul au lieu de la tranche du cumul annuel, ce qui serait faux).
|
||||
var baremeAnnuel = await _baremeService.CalculerMontantAsync(
|
||||
annee, vehicule.PuissanceFiscale, totalAnnuelPartiel, cancellationToken);
|
||||
|
||||
var montantBaremePlein = Math.Round(bareme.Montant, 2, MidpointRounding.AwayFromZero);
|
||||
var montantMoisSelonTrancheAnnuelle = totalMoisAffiche * baremeAnnuel.Coefficient + baremeAnnuel.Base;
|
||||
|
||||
// Montant_Previsionnel = 80% du barème plein, arrondi à 2 décimales (indicatif — Exigences 13.5/13.6).
|
||||
var montantPrevisionnel = Math.Round(bareme.Montant * TauxPrevisionnel, 2, MidpointRounding.AwayFromZero);
|
||||
// Montant_Previsionnel_Mensuel = 80% du barème (tranche du cumul annuel) appliqué au mois affiché.
|
||||
var montantPrevisionnelMensuel = Math.Round(
|
||||
montantMoisSelonTrancheAnnuelle * TauxPrevisionnel, 2, MidpointRounding.AwayFromZero);
|
||||
|
||||
// Montant_Previsionnel_Annuel = 100% du barème plein appliqué au cumul annuel.
|
||||
var montantPrevisionnelAnnuel = Math.Round(baremeAnnuel.Montant, 2, MidpointRounding.AwayFromZero);
|
||||
|
||||
return new MontantPrevisionnelDto(
|
||||
Annee: annee,
|
||||
Mois: mois,
|
||||
VehiculeId: vehiculeId,
|
||||
TotalAnnuelPartielKm: Math.Round(totalAnnuelPartiel, 2, MidpointRounding.AwayFromZero),
|
||||
MontantBaremePlein: montantBaremePlein,
|
||||
MontantPrevisionnel: montantPrevisionnel);
|
||||
TotalMoisKm: totalMoisAffiche,
|
||||
TotalAnnuelPartielKm: totalAnnuelPartiel,
|
||||
MontantPrevisionnelMensuel: montantPrevisionnelMensuel,
|
||||
MontantPrevisionnelAnnuel: montantPrevisionnelAnnuel);
|
||||
}
|
||||
|
||||
private static MontantRegleDto ToDto(MontantRegle m) =>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SchotzIadKmTracker.Api.Data;
|
||||
using SchotzIadKmTracker.Api.Models.Entities;
|
||||
using SchotzIadKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class PrecisionDistanceService : IPrecisionDistanceService
|
||||
{
|
||||
// La configuration est un singleton : une seule ligne dont l'identifiant est toujours 1.
|
||||
private const int ParametresId = 1;
|
||||
|
||||
private readonly AppDbContext _context;
|
||||
|
||||
public PrecisionDistanceService(AppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> ObtenirAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var parametres = await GetOrCreateAsync(cancellationToken);
|
||||
return parametres.PrecisionDistanceKm;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> DefinirAsync(int valeur, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (valeur is not (0 or 1 or 2))
|
||||
{
|
||||
throw new PrecisionDistanceInvalideException(
|
||||
"La précision des distances doit être 0, 1 ou 2 décimale(s).");
|
||||
}
|
||||
|
||||
var parametres = await GetOrCreateAsync(cancellationToken);
|
||||
parametres.PrecisionDistanceKm = valeur;
|
||||
parametres.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return parametres.PrecisionDistanceKm;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public decimal ArrondirDistance(decimal distanceBrute, int precision)
|
||||
{
|
||||
var facteur = (decimal)Math.Pow(10, precision);
|
||||
return Math.Ceiling(distanceBrute * facteur) / facteur;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Récupère la configuration singleton (Id = 1) ou la crée à la volée si elle est absente,
|
||||
/// avec la valeur par défaut (2 décimales).
|
||||
/// </summary>
|
||||
private async Task<ParametresApplication> GetOrCreateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var parametres = await _context.ParametresApplication
|
||||
.FirstOrDefaultAsync(p => p.Id == ParametresId, cancellationToken);
|
||||
if (parametres is not null)
|
||||
{
|
||||
return parametres;
|
||||
}
|
||||
|
||||
parametres = new ParametresApplication { Id = ParametresId };
|
||||
await _context.ParametresApplication.AddAsync(parametres, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return parametres;
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,18 @@ public class RemboursementService : IRemboursementService
|
||||
private readonly AppDbContext _context;
|
||||
private readonly IBaremeService _baremeService;
|
||||
private readonly IMontantRegleService _montantRegleService;
|
||||
private readonly IPrecisionDistanceService _precisionDistanceService;
|
||||
|
||||
public RemboursementService(
|
||||
AppDbContext context,
|
||||
IBaremeService baremeService,
|
||||
IMontantRegleService montantRegleService)
|
||||
IMontantRegleService montantRegleService,
|
||||
IPrecisionDistanceService precisionDistanceService)
|
||||
{
|
||||
_context = context;
|
||||
_baremeService = baremeService;
|
||||
_montantRegleService = montantRegleService;
|
||||
_precisionDistanceService = precisionDistanceService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -32,6 +35,9 @@ public class RemboursementService : IRemboursementService
|
||||
.FirstOrDefaultAsync(v => v.Id == vehiculeId, cancellationToken)
|
||||
?? throw new VehiculeNotFoundException($"Le véhicule d'identifiant {vehiculeId} est introuvable.");
|
||||
|
||||
// Précision d'arrondi courante (Exigence 4.1) : lue une fois pour toute la requête.
|
||||
var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken);
|
||||
|
||||
// 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.
|
||||
@@ -39,13 +45,15 @@ public class RemboursementService : IRemboursementService
|
||||
.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.
|
||||
// distance EFFECTIVE (arrondie AVANT doublement si A/R — Exigence 4.2) 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)));
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => g.Sum(t => _precisionDistanceService.ArrondirDistance(t.Distance ?? 0m, precision) * (t.AllerRetour ? 2m : 1m)));
|
||||
|
||||
// Total kilométrique annuel COMPLET (détermine la tranche — Exigence 14.5, Property 27).
|
||||
var totalKmAnnuel = kmMoisMap.Values.Sum();
|
||||
@@ -78,7 +86,7 @@ public class RemboursementService : IRemboursementService
|
||||
|
||||
lignes.Add(new LigneSyntheseMoisDto(
|
||||
Mois: mois,
|
||||
KmMois: Math.Round(kmMois, 2, MidpointRounding.AwayFromZero),
|
||||
KmMois: kmMois,
|
||||
FraisPaye: fraisPaye,
|
||||
FraisReelMensuel: fraisReelMensuel));
|
||||
}
|
||||
@@ -97,7 +105,7 @@ public class RemboursementService : IRemboursementService
|
||||
BaseTranche: baseTranche,
|
||||
TrancheLibelle: trancheLibelle,
|
||||
Mois: lignes,
|
||||
TotalKmAnnuel: Math.Round(totalKmAnnuel, 2, MidpointRounding.AwayFromZero),
|
||||
TotalKmAnnuel: totalKmAnnuel,
|
||||
TotalPaye: totalPaye,
|
||||
MontantReel: montantReel,
|
||||
Regularisation: regularisation);
|
||||
|
||||
@@ -15,10 +15,12 @@ public class ResumeService : IResumeService
|
||||
private const int AnneeMax = 2100;
|
||||
|
||||
private readonly AppDbContext _context;
|
||||
private readonly IPrecisionDistanceService _precisionDistanceService;
|
||||
|
||||
public ResumeService(AppDbContext context)
|
||||
public ResumeService(AppDbContext context, IPrecisionDistanceService precisionDistanceService)
|
||||
{
|
||||
_context = context;
|
||||
_precisionDistanceService = precisionDistanceService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -28,9 +30,10 @@ public class ResumeService : IResumeService
|
||||
ValiderAnnee(annee);
|
||||
ValiderMois(mois);
|
||||
|
||||
var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken);
|
||||
var trajets = await ChargerTrajetsDuMoisAsync(annee, mois, vehiculeId, cancellationToken);
|
||||
|
||||
return ConstruireResumeMensuel(annee, mois, trajets);
|
||||
return ConstruireResumeMensuel(annee, mois, trajets, precision);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -39,6 +42,8 @@ public class ResumeService : IResumeService
|
||||
{
|
||||
ValiderAnnee(annee);
|
||||
|
||||
var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken);
|
||||
|
||||
// Charge une seule fois tous les trajets de l'année, puis répartit par mois.
|
||||
var query = _context.Trajets
|
||||
.AsNoTracking()
|
||||
@@ -58,11 +63,11 @@ public class ResumeService : IResumeService
|
||||
.Where(t => t.Date.Month == mois)
|
||||
.ToList();
|
||||
|
||||
resumesMensuels.Add(ConstruireResumeMensuel(annee, mois, trajetsDuMois));
|
||||
resumesMensuels.Add(ConstruireResumeMensuel(annee, mois, trajetsDuMois, precision));
|
||||
}
|
||||
|
||||
// Total annuel = somme des 12 totaux mensuels (Exigences 4.3, 4.7, 6.6).
|
||||
var totalAnnuel = Arrondir(resumesMensuels.Sum(r => r.TotalKm));
|
||||
// Total annuel = somme des 12 totaux mensuels, déjà arrondis (Exigences 4.3, 4.7, 6.6, 5.2, 5.3).
|
||||
var totalAnnuel = resumesMensuels.Sum(r => r.TotalKm);
|
||||
|
||||
return new ResumeAnnuelDto(annee, resumesMensuels, totalAnnuel);
|
||||
}
|
||||
@@ -93,17 +98,39 @@ public class ResumeService : IResumeService
|
||||
|
||||
/// <summary>
|
||||
/// Construit un <see cref="ResumeMensuelDto"/> à partir d'une liste de Trajets déjà triée.
|
||||
/// Le total est la somme des distances (null → 0) arrondie à 2 décimales (Property 1).
|
||||
/// Chaque distance est arrondie (Precision_Distance) AVANT doublement aller/retour ; le total
|
||||
/// est la somme de ces valeurs déjà arrondies, sans second arrondi (Property 1, Property 2, Property 3).
|
||||
/// </summary>
|
||||
private static ResumeMensuelDto ConstruireResumeMensuel(int annee, int mois, IReadOnlyList<Trajet> trajets)
|
||||
private ResumeMensuelDto ConstruireResumeMensuel(
|
||||
int annee, int mois, IReadOnlyList<Trajet> trajets, int precision)
|
||||
{
|
||||
var trajetsDto = trajets.Select(ToDto).ToList();
|
||||
// Total = somme des distances EFFECTIVES (doublées si aller/retour), null → 0 (Property 1).
|
||||
var totalKm = Arrondir(trajets.Sum(t => t.DistanceEffective ?? 0m));
|
||||
// Calculée une seule fois par trajet, puis réutilisée pour le DTO et la somme du total
|
||||
// afin de garantir la même valeur (Distance_Effective_Arrondie) aux deux endroits.
|
||||
var distancesArrondies = trajets.Select(t => (t, distance: DistanceEffectiveArrondie(t, precision))).ToList();
|
||||
|
||||
var trajetsDto = distancesArrondies.Select(x => ToDto(x.t, x.distance)).ToList();
|
||||
// Total = somme des Distance_Effective_Arrondie déjà calculées par trajet, null → 0 (Property 1, Property 3).
|
||||
var totalKm = distancesArrondies.Sum(x => x.distance ?? 0m);
|
||||
|
||||
return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calcule la Distance_Effective_Arrondie d'un trajet : arrondi (Precision_Distance) appliqué
|
||||
/// à la distance SIMPLE brute AVANT doublement aller/retour (Exigence 4.1, 4.2, 4.3). null si la
|
||||
/// distance brute n'a pas pu être calculée (Exigence 2.4 du résumé existant).
|
||||
/// </summary>
|
||||
private decimal? DistanceEffectiveArrondie(Trajet t, int precision)
|
||||
{
|
||||
if (!t.Distance.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var distanceArrondie = _precisionDistanceService.ArrondirDistance(t.Distance.Value, precision);
|
||||
return t.AllerRetour ? distanceArrondie * 2m : distanceArrondie;
|
||||
}
|
||||
|
||||
private void ValiderMois(int mois)
|
||||
{
|
||||
if (mois < MoisMin || mois > MoisMax)
|
||||
@@ -122,17 +149,15 @@ public class ResumeService : IResumeService
|
||||
}
|
||||
}
|
||||
|
||||
private static decimal Arrondir(decimal valeur) =>
|
||||
Math.Round(valeur, 2, MidpointRounding.AwayFromZero);
|
||||
|
||||
private static TrajetDto ToDto(Trajet t) =>
|
||||
private static TrajetDto ToDto(Trajet t, decimal? distanceEffectiveArrondie) =>
|
||||
new(
|
||||
t.Id,
|
||||
t.Date,
|
||||
t.AdresseDepart,
|
||||
t.AdresseArrivee,
|
||||
// Distance EFFECTIVE (doublée si aller/retour), alignée sur le TrajetDto commun.
|
||||
t.DistanceEffective,
|
||||
// Distance_Effective_Arrondie (arrondi Precision_Distance puis doublement si aller/retour),
|
||||
// alignée sur le TrajetDto commun (au lieu de DistanceEffective brut).
|
||||
distanceEffectiveArrondie,
|
||||
t.Commentaire,
|
||||
t.Position,
|
||||
t.VehiculeId,
|
||||
|
||||
@@ -9,11 +9,14 @@ public class TotauxService : ITotauxService
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
private readonly IVehiculeService _vehiculeService;
|
||||
private readonly IPrecisionDistanceService _precisionDistanceService;
|
||||
|
||||
public TotauxService(AppDbContext context, IVehiculeService vehiculeService)
|
||||
public TotauxService(
|
||||
AppDbContext context, IVehiculeService vehiculeService, IPrecisionDistanceService precisionDistanceService)
|
||||
{
|
||||
_context = context;
|
||||
_vehiculeService = vehiculeService;
|
||||
_precisionDistanceService = precisionDistanceService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -24,6 +27,10 @@ public class TotauxService : ITotauxService
|
||||
var vehiculeCible = vehiculeId
|
||||
?? (await _vehiculeService.GetVehiculeCourantAsync(cancellationToken)).Id;
|
||||
|
||||
// Précision courante (0/1/2 décimales) : lue une fois par appel, stable pour tous les
|
||||
// totaux calculés ci-dessous (Exigences 4.1, 4.2, 4.3).
|
||||
var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken);
|
||||
|
||||
// Trajets du véhicule pour l'année sélectionnée (Property 21 : filtrage par véhicule).
|
||||
// Distance nullable → 0 lors des sommes (traitée côté client après projection).
|
||||
var trajetsAnnee = await _context.Trajets
|
||||
@@ -34,9 +41,10 @@ public class TotauxService : ITotauxService
|
||||
.Select(t => new { t.Date, t.Distance, t.AllerRetour })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Distance comptée (effective) : distance simple × 2 si aller/retour, null → 0.
|
||||
static decimal Effective(decimal? distance, bool allerRetour) =>
|
||||
distance.GetValueOrDefault() * (allerRetour ? 2m : 1m);
|
||||
// Distance comptée (effective) : distance simple arrondie à la précision courante,
|
||||
// PUIS doublée si aller/retour (Exigence 4.2 — arrondi avant doublement), null → 0.
|
||||
decimal Effective(decimal? distance, bool allerRetour) =>
|
||||
_precisionDistanceService.ArrondirDistance(distance.GetValueOrDefault(), precision) * (allerRetour ? 2m : 1m);
|
||||
|
||||
// TotalJour : somme des distances effectives de la journée sélectionnée (Exigence 12.7).
|
||||
var totalJour = trajetsAnnee
|
||||
@@ -54,12 +62,15 @@ public class TotauxService : ITotauxService
|
||||
.Where(t => t.Date.Month <= date.Month)
|
||||
.Sum(t => Effective(t.Distance, t.AllerRetour));
|
||||
|
||||
// Les distances sommées sont déjà arrondies à la précision courante (via
|
||||
// ArrondirDistance) : aucun second arrondi forcé à 2 décimales n'est appliqué ici
|
||||
// (Exigences 5.1, 5.4).
|
||||
return new TotauxDto(
|
||||
Date: date,
|
||||
VehiculeId: vehiculeCible,
|
||||
TotalJour: Math.Round(totalJour, 2, MidpointRounding.AwayFromZero),
|
||||
TotalMois: Math.Round(totalMois, 2, MidpointRounding.AwayFromZero),
|
||||
TotalAnnuelPartiel: Math.Round(totalAnnuelPartiel, 2, MidpointRounding.AwayFromZero));
|
||||
TotalJour: totalJour,
|
||||
TotalMois: totalMois,
|
||||
TotalAnnuelPartiel: totalAnnuelPartiel);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user