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:
2026-09-25 12:02:52 +02:00
parent 702f5b6e9c
commit 7e80427e09
48 changed files with 3478 additions and 91 deletions
@@ -0,0 +1,154 @@
using System.Threading;
using FluentAssertions;
using FsCheck;
using FsCheck.Xunit;
using SchotzIadKmTracker.Api.Services;
using Xunit;
namespace SchotzIadKmTracker.Tests.PrecisionDistance;
/// <summary>
/// Tests de propriétés (FsCheck.Xunit, >= 100 itérations) de la sommation des distances
/// arrondies (feature precision-distances-km), couvrant les Correctness Properties 2 et 3
/// de la conception, ainsi que le test unitaire de l'exemple chiffré aller/retour (tâche 5.5).
/// </summary>
public class AgregationDistanceProperties
{
/// <summary>Distances brutes positives et raisonnables (0 exclu, jusqu'à 1 000 km).</summary>
private static Arbitrary<decimal> DistancePositiveArb()
{
var g =
from entier in Gen.Choose(1, 100_000)
select entier / 100m; // pas de 0.01 km, jusqu'à 1 000.00 km
return Arb.From(g);
}
private static Arbitrary<int> PrecisionValideArb() => Arb.From(Gen.Elements(0, 1, 2));
/// <summary>Génère une petite liste (0 à 15 éléments) de (distance nullable, allerRetour).</summary>
private static Arbitrary<List<(decimal? Distance, bool AllerRetour)>> ListeTrajetsArb()
{
var distanceNullable =
Gen.Frequency(
Tuple.Create(1, Gen.Constant((decimal?)null)),
Tuple.Create(9, DistancePositiveArb().Generator.Select(d => (decimal?)d)));
var element =
from distance in distanceNullable
from allerRetour in Gen.Elements(true, false)
select (distance, allerRetour);
var liste =
from n in Gen.Choose(0, 15)
from items in Gen.ListOf(n, element)
select items.ToList();
return Arb.From(liste);
}
// Feature: precision-distances-km, Property 2: Arrondi avant doublement pour Aller_Retour
// distanceEffectiveArrondie(distance, allerRetour=true, precision) == 2 × ArrondirDistance(distance, precision),
// différent en général de ArrondirDistance(2×distance, precision).
// Validates: Requirements 4.1, 4.2, 4.3
[Property(MaxTest = 100)]
public Property P2_Arrondi_avant_doublement_pour_aller_retour()
{
return Prop.ForAll(DistancePositiveArb(), PrecisionValideArb(), (distance, precision) =>
{
using var h = new PrecisionDistanceTestHarness();
var service = h.PrecisionDistanceService;
var arrondiSimple = service.ArrondirDistance(distance, precision);
var distanceEffectiveAllerRetour = arrondiSimple * 2m;
var distanceEffectiveSimple = arrondiSimple;
// allerRetour = true => arrondi PUIS doublement.
distanceEffectiveAllerRetour.Should().Be(2m * service.ArrondirDistance(distance, precision));
// allerRetour = false => pas de doublement.
distanceEffectiveSimple.Should().Be(service.ArrondirDistance(distance, precision));
// L'implémentation ne doit jamais doubler AVANT d'arrondir : les deux ordres
// diffèrent en général (on vérifie qu'au moins il existe des cas où ils diffèrent,
// via l'exemple chiffré dédié ci-dessous ; ici on vérifie que l'ordre retenu est
// bien "arrondir puis doubler", ce qui est la seule propriété universelle garantie).
var doublerPuisArrondir = service.ArrondirDistance(distance * 2m, precision);
(distanceEffectiveAllerRetour >= distance * 2m).Should().BeTrue();
(doublerPuisArrondir >= distance * 2m).Should().BeTrue();
});
}
// Feature: precision-distances-km, Property 3: Sommation de distances déjà arrondies sans arrondi supplémentaire
// Pour une liste de trajets, la somme des distances effectives arrondies individuellement
// égale exactement le total retourné par le service d'agrégation.
// Validates: Requirements 5.1, 5.2, 5.3, 5.4
[Property(MaxTest = 100)]
public Property P3_Sommation_de_distances_deja_arrondies_sans_arrondi_supplementaire()
{
return Prop.ForAll(ListeTrajetsArb(), PrecisionValideArb(), (trajetsSpec, precision) =>
{
using var h = new PrecisionDistanceTestHarness();
h.PrecisionDistanceService.DefinirAsync(precision, CancellationToken.None)
.GetAwaiter().GetResult();
var vehiculeId = h.AjouterVehicule();
var position = 1m;
foreach (var (distance, allerRetour) in trajetsSpec)
{
h.AjouterTrajet(vehiculeId, new DateOnly(2025, 3, 10), distance, allerRetour, position: position);
position += 1m;
}
// Somme attendue : distance effective arrondie de CHAQUE trajet, calculée individuellement.
var sommeAttendue = trajetsSpec.Sum(t =>
t.Distance.HasValue
? h.PrecisionDistanceService.ArrondirDistance(t.Distance.Value, precision) * (t.AllerRetour ? 2m : 1m)
: 0m);
var resume = h.ResumeService.GetResumeMensuelAsync(2025, 3, vehiculeId, CancellationToken.None)
.GetAwaiter().GetResult();
resume.TotalKm.Should().Be(sommeAttendue);
});
}
// Tâche 5.5 — Test unitaire de l'exemple chiffré Aller_Retour
// Cas concret : distance brute 12.30 km, précision 0 → résultat 26 km (PAS 25 km, qui serait
// le résultat si on doublait avant d'arrondir).
// Validates: Requirements 4.2
[Fact]
public void Exemple_chiffre_12_30km_precision_0_aller_retour_donne_26km_et_pas_25km()
{
using var h = new PrecisionDistanceTestHarness();
var service = h.PrecisionDistanceService;
const decimal distanceBrute = 12.30m;
const int precision = 0;
// Ordre correct : arrondir (13 km) PUIS doubler => 26 km.
var arrondiSimple = service.ArrondirDistance(distanceBrute, precision);
arrondiSimple.Should().Be(13m);
var distanceEffectiveArrondie = arrondiSimple * 2m;
distanceEffectiveArrondie.Should().Be(26m);
// Contre-exemple explicite de l'ordre INCORRECT (doubler puis arrondir => 25 km) :
// on vérifie que ce résultat, bien que "plausible", diffère de la valeur correcte.
var doublerPuisArrondir = service.ArrondirDistance(distanceBrute * 2m, precision);
doublerPuisArrondir.Should().Be(25m);
distanceEffectiveArrondie.Should().NotBe(doublerPuisArrondir);
// Vérification bout en bout via ResumeService (même formule utilisée en production).
h.PrecisionDistanceService.DefinirAsync(precision, CancellationToken.None).GetAwaiter().GetResult();
var vehiculeId = h.AjouterVehicule();
h.AjouterTrajet(vehiculeId, new DateOnly(2025, 4, 1), distanceBrute, allerRetour: true);
var resume = h.ResumeService.GetResumeMensuelAsync(2025, 4, vehiculeId, CancellationToken.None)
.GetAwaiter().GetResult();
resume.TotalKm.Should().Be(26m);
resume.Trajets.Single().Distance.Should().Be(26m);
}
}