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 @@
{"specId": "b190f621-9d51-491c-877a-7649d38590de", "workflowType": "requirements-first", "specType": "feature"}
@@ -0,0 +1,781 @@
# Document de Conception Technique — Précision des distances (km)
## Overview
Cette fonctionnalité ajoute un paramètre global **Precision_Distance** (0, 1 ou 2 décimales) qui pilote un arrondi supérieur (ceiling) systématique appliqué à l'affichage et à l'agrégation de toutes les distances de l'application : liste des trajets, panneau des totaux, carte, export PDF, et calculs de barème kilométrique (Montant_Previsionnel_Mensuel, Montant_Previsionnel_Annuel, Montant_Reel, Frais_Reel_Mensuel, Regularisation).
Elle modifie également `IMontantRegleService` : le Montant_Previsionnel_Mensuel (80%) porte désormais uniquement sur le mois affiché (et non plus sur le Total_Annuel_Partiel), et une nouvelle valeur Montant_Previsionnel_Annuel (100%, sans minoration) est introduite, appliquée au Total_Annuel_Partiel.
Cette conception **s'appuie sur l'architecture existante** décrite dans `.kiro/specs/schotziad-km-tracker/design.md` (ASP.NET Core 8 / EF Core / SQLite côté backend, Angular 22 zoneless standalone côté frontend) et ne la remet pas en cause : elle ajoute une entité de configuration, un service de précision, un endpoint, et modifie des points de calcul existants identifiés dans le code réel (`ResumeService`, `TotauxService`, `MontantRegleService`, `RemboursementService`, `PdfService`, `CarteTrajetsComponent`, `TrajetListComponent`, `PanneauTotauxComponent`, `MontantsReglesComponent`).
> **Important** : l'arrondi ORS/Haversine existant dans `ORSClient.cs` (`Math.Round(distanceMeters / 1000, 2, MidpointRounding.AwayFromZero)`) reste inchangé — c'est l'arrondi de **calcul brut** stocké en base (colonne `Trajet.Distance`). Le nouvel arrondi Precision_Distance est un **arrondi d'affichage/agrégation en aval**, appliqué à la volée sur la valeur lue, jamais persisté sur `Trajet.Distance`.
---
## Architecture
### Vue d'ensemble des points d'impact
```mermaid
graph TB
subgraph "Backend ASP.NET Core"
PARAM_CTRL[ParametresApplicationController]
PARAM_SVC[PrecisionDistanceService<br/>ObtenirAsync / DefinirAsync / ArrondirDistance]
PARAM_DB[(ParametresApplication<br/>singleton)]
RESUME[ResumeService]
TOTAUX[TotauxService]
MONTANT[MontantRegleService]
REMB[RemboursementService]
BAREME[BaremeService]
PDF[PdfService]
PARAM_CTRL --> PARAM_SVC
PARAM_SVC --> PARAM_DB
RESUME --> PARAM_SVC
TOTAUX --> PARAM_SVC
MONTANT --> PARAM_SVC
REMB --> PARAM_SVC
PDF --> PARAM_SVC
MONTANT --> BAREME
REMB --> BAREME
REMB --> MONTANT
PDF --> RESUME
PDF --> REMB
PDF --> MONTANT
PDF --> BAREME
end
subgraph "Frontend Angular"
PARAM_SVC_FE[ParametresApplicationService<br/>signal precisionDistance]
PARAM_COMP[ParametresApplicationComponent]
LISTE[TrajetListComponent]
TOTAUX_COMP[PanneauTotauxComponent]
CARTE[CarteTrajetsComponent]
MONTANTS_COMP[MontantsReglesComponent]
ARRONDI_FE[arrondirDistance util]
PARAM_COMP --> PARAM_SVC_FE
LISTE --> PARAM_SVC_FE
TOTAUX_COMP --> PARAM_SVC_FE
CARTE --> PARAM_SVC_FE
MONTANTS_COMP --> PARAM_SVC_FE
LISTE --> ARRONDI_FE
TOTAUX_COMP --> ARRONDI_FE
CARTE --> ARRONDI_FE
end
PARAM_SVC_FE -- "HTTP GET/PUT" --> PARAM_CTRL
```
### Décision d'architecture : table dédiée à une seule ligne (singleton), pas de table clé-valeur générique
**Options considérées :**
1. **Table clé-valeur générique** (`ParametreApplication { Cle, Valeur }`) — flexible pour ajouter de futurs paramètres sans migration, mais nécessite un parsing/validation par clé côté application (perte du typage fort EF Core), et introduit une indirection pour un besoin actuel limité à un seul paramètre entier.
2. **Table dédiée à une seule ligne (singleton)**, sur le modèle exact de `Profil` et `Compte` déjà présents dans `AppDbContext` (`Id` fixe = 1, `ValueGeneratedNever()`, index unique `UX_*_Singleton`).
**Décision retenue : option 2**, pour cohérence stricte avec les conventions existantes du projet (`Profil`, `Compte` sont déjà des singletons typés) et parce que le besoin actuel est un unique paramètre entier borné (0/1/2). Si d'autres paramètres globaux apparaissent plus tard, cette même table `ParametresApplication` pourra recevoir de nouvelles colonnes (comme `Profil` a reçu ses champs successivement) sans qu'une refonte soit nécessaire.
### Décision d'architecture : arrondi ceiling centralisé dans une méthode utilitaire pure, appelée à la lecture
L'arrondi n'est **jamais** appliqué à l'écriture (la colonne `Trajet.Distance` n'est jamais modifiée par ce paramètre — Exigence 1.8). Il est appliqué :
- **côté backend**, à chaque endroit où une distance ou un total est **lu puis exposé** (DTO de réponse, PDF) ;
- **côté frontend**, à l'affichage, en réutilisant la valeur `precisionDistance` courante partagée par un signal de service, pour un rendu réactif sans aller-retour serveur à chaque changement de sélection (mois, véhicule, journée).
Cela garantit l'Exigence 9.4 (même Distance_Effective_Arrondie pour un même Trajet, à précision égale, dans la liste, le panneau, la carte, le PDF et le barème) : une seule fonction d'arrondi (`ArrondirDistance` côté backend, `arrondirDistance` côté frontend), avec la même formule.
---
## Data Models
### Entité `ParametresApplication` (nouvelle — backend)
```csharp
namespace SchotzIadKmTracker.Api.Models.Entities;
/// <summary>
/// Configuration globale de l'application (singleton — une seule ligne, Id = 1).
/// Sur le même modèle que <see cref="Profil"/> et <see cref="Compte"/>.
/// </summary>
public class ParametresApplication
{
/// <summary>Identifiant fixe (1) : la configuration est un singleton.</summary>
public int Id { get; set; } = 1;
/// <summary>
/// Nombre de décimales (0, 1 ou 2) utilisé pour l'arrondi supérieur des distances à
/// l'affichage et dans les agrégats. Défaut 2 (Exigence 1.2). Valeur validée par
/// <see cref="Services.IPrecisionDistanceService"/> avant écriture — jamais persistée
/// hors de {0, 1, 2}.
/// </summary>
public int PrecisionDistanceKm { get; set; } = 2;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
```
### Enregistrement dans `AppDbContext`
```csharp
public DbSet<ParametresApplication> ParametresApplication => Set<ParametresApplication>();
```
```csharp
// --- ParametresApplication (singleton Id = 1) ---
modelBuilder.Entity<ParametresApplication>(entity =>
{
entity.ToTable("ParametresApplication", t =>
t.HasCheckConstraint("CK_ParametresApplication_PrecisionDistanceKm", "\"PrecisionDistanceKm\" IN (0, 1, 2)"));
entity.HasKey(p => p.Id);
entity.Property(p => p.Id).ValueGeneratedNever();
entity.HasIndex(p => p.Id, "UX_ParametresApplication_Singleton").IsUnique();
});
```
### Migration EF Core
Nouvelle migration (nom à la suite des migrations existantes, ex. `20260916000000_AjoutParametresApplication`), créant la table `ParametresApplication` avec la contrainte CHECK ci-dessus. Aucune donnée à migrer (table vide au départ ; la ligne singleton est créée à la volée, comme `Profil`, au premier accès — cf. `PrecisionDistanceService.ObtenirAsync`).
### Schéma SQLite
```sql
CREATE TABLE ParametresApplication (
Id INTEGER PRIMARY KEY,
PrecisionDistanceKm INTEGER NOT NULL DEFAULT 2 CHECK (PrecisionDistanceKm IN (0, 1, 2)),
UpdatedAt TEXT NOT NULL DEFAULT (datetime('now'))
);
```
---
## API
### DTOs (nouveau fichier `Models/DTOs/ParametresApplicationDtos.cs`)
```csharp
namespace SchotzIadKmTracker.Api.Models.DTOs;
/// <summary>Configuration globale exposée en lecture.</summary>
public record ParametresApplicationDto(int PrecisionDistanceKm);
/// <summary>Corps de requête pour modifier la précision des distances.</summary>
public record SavePrecisionDistanceDto(int PrecisionDistanceKm);
```
### Endpoints (`Controllers/ParametresApplicationController.cs`)
Suit exactement les conventions de `BaremeController` (route `api/...`, `[ApiController]`, autorisation JWT globale héritée du filtre configuré dans `Program.cs`, pas d'attribut `[Authorize]` explicite requis — seul `AuthController` porte `[AllowAnonymous]`).
| Méthode | Route | Description | Codes |
|---|---|---|---|
| GET | `/api/parametres-application` | Retourne la configuration courante (crée la ligne singleton avec la valeur par défaut 2 si absente) | 200 |
| PUT | `/api/parametres-application` | Modifie `PrecisionDistanceKm` | 200, 400 |
```csharp
[ApiController]
[Route("api/parametres-application")]
public class ParametresApplicationController : ControllerBase
{
private readonly IPrecisionDistanceService _precisionDistanceService;
public ParametresApplicationController(IPrecisionDistanceService precisionDistanceService)
{
_precisionDistanceService = precisionDistanceService;
}
[HttpGet]
[ProducesResponseType(typeof(ParametresApplicationDto), StatusCodes.Status200OK)]
public async Task<ActionResult<ParametresApplicationDto>> Get(CancellationToken cancellationToken)
{
var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken);
return Ok(new ParametresApplicationDto(precision));
}
[HttpPut]
[ProducesResponseType(typeof(ParametresApplicationDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<ParametresApplicationDto>> Put(
[FromBody] SavePrecisionDistanceDto dto, CancellationToken cancellationToken)
{
try
{
var precision = await _precisionDistanceService.DefinirAsync(dto.PrecisionDistanceKm, cancellationToken);
return Ok(new ParametresApplicationDto(precision));
}
catch (PrecisionDistanceInvalideException ex)
{
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
}
}
}
```
L'authentification est garantie par le filtre d'autorisation global déjà enregistré dans `Program.cs` (`AuthorizeFilter` appliqué à tous les contrôleurs MVC) : sans jeton JWT valide, la requête reçoit un **401** avant d'atteindre le contrôleur (Exigences 1.7, 2.6 — même comportement que `BaremeController`, `ProfilController`, etc., aucune configuration supplémentaire requise).
### Validation (400)
Portée par le service (`PrecisionDistanceService.DefinirAsync`), pas par FluentValidation, car la validation est une règle métier simple (appartenance à `{0, 1, 2}`) sur un endpoint à faible complexité — cohérent avec `MontantRegleService` qui valide aussi en profondeur dans le service (défense en profondeur, cf. `MontantRegleNegatifException`). Une nouvelle exception `PrecisionDistanceInvalideException` (`Services/Exceptions/PrecisionDistanceInvalideException.cs`) suit le même modèle que `MontantRegleNegatifException`.
---
## Components and Interfaces
### Backend — nouveau service `IPrecisionDistanceService` / `PrecisionDistanceService`
```csharp
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);
}
```
```csharp
public class PrecisionDistanceService : IPrecisionDistanceService
{
private const int ParametresId = 1;
private readonly AppDbContext _context;
public PrecisionDistanceService(AppDbContext context) => _context = context;
public async Task<int> ObtenirAsync(CancellationToken cancellationToken = default)
{
var parametres = await GetOrCreateAsync(cancellationToken);
return parametres.PrecisionDistanceKm;
}
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;
}
public decimal ArrondirDistance(decimal distanceBrute, int precision)
{
var facteur = (decimal)Math.Pow(10, precision);
return Math.Ceiling(distanceBrute * facteur) / facteur;
}
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;
}
}
```
Enregistrement dans `Program.cs`, à côté des autres services scoped :
```csharp
builder.Services.AddScoped<IPrecisionDistanceService, PrecisionDistanceService>();
```
### Points de consommation backend (injection de `IPrecisionDistanceService`)
Tous les services suivants injectent `IPrecisionDistanceService` en plus de leurs dépendances actuelles, appellent `ObtenirAsync()` une fois par requête (le résultat est stable pendant la durée de la requête), puis appliquent `ArrondirDistance` **avant sommation** (jamais après) :
| Service | Méthode(s) concernée(s) | Changement |
|---|---|---|
| `ResumeService` | `ConstruireResumeMensuel` (privée, appelée par `GetResumeMensuelAsync`/`GetResumeAnnuelAsync`) | Pour chaque trajet : `Distance_Effective_Arrondie = ArrondirDistance(Distance, precision) × (AllerRetour ? 2 : 1)` (arrondi AVANT doublement — Exigence 4.2), puis somme de ces valeurs déjà arrondies pour `TotalKm` (Exigence 5.2/5.3). Le `TrajetDto.Distance` exposé devient lui aussi `Distance_Effective_Arrondie` (au lieu de `DistanceEffective` brut). |
| `TotauxService` | `GetTotauxAsync` | La fonction locale `Effective(distance, allerRetour)` devient `ArrondirDistance(distance, precision) × (allerRetour ? 2 : 1)` ; `TotalJour`, `TotalMois`, `TotalAnnuelPartiel` sont des sommes de valeurs déjà arrondies (Exigence 5.1, 5.4), sans nouvel arrondi après la somme (le `Math.Round(..., 2, ...)` final existant est **supprimé** — les distances sommées sont déjà à la précision voulue et n'ont plus besoin d'un second arrondi à 2 décimales). |
| `MontantRegleService` | `GetMontantPrevisionnelAsync` | Voir section dédiée ci-dessous (Requirement 7/8). |
| `RemboursementService` | `GetRegularisationAnnuelleAsync` | `kmMoisMap` est construit à partir de `ArrondirDistance(Distance, precision) × (AllerRetour ? 2 : 1)` par trajet (au lieu de la distance brute), donc `totalKmAnnuel` (Exigence 6.3, 6.6) et chaque `KmMois` (Exigence 6.4) sont des sommes de distances déjà arrondies. |
| `PdfService` | `AssemblerModeleMensuelAsync`, tables du rapport annuel (via `RemboursementService`/`ResumeService` déjà corrigés) | Consomme les résumés/régularisations déjà arrondis en amont ; aucun calcul de distance supplémentaire à modifier directement — la cohérence vient de la réutilisation des services corrigés (Exigence 9.2, 9.3). Voir section « Cohérence PDF/Carte » plus bas pour le détail des points d'appel exacts. |
| `BaremeService` | `CalculerMontantAsync` | Ne change pas : reçoit déjà un `totalKm` en paramètre. Ce sont ses **appelants** (`MontantRegleService`, `RemboursementService`) qui doivent désormais lui passer un total déjà arrondi (Exigence 6.6). |
### Requirement 7/8 — `MontantRegleService.GetMontantPrevisionnelAsync`
Signature actuelle (`MontantRegleService.cs`) : calcule un seul `MontantPrevisionnelDto` avec `MontantPrevisionnel` = 80% du barème appliqué au **Total_Annuel_Partiel**. Cette conception est remplacée par :
```csharp
Task<MontantPrevisionnelDto> GetMontantPrevisionnelAsync(
int annee, int mois, int vehiculeId, CancellationToken cancellationToken = default);
```
avec un **nouveau contenu de calcul** (signature externe inchangée pour ne pas casser le contrat déjà consommé par `BaremeService.getMontantPrevisionnel` côté frontend — seuls les champs du DTO retourné changent) :
1. Charger les trajets du véhicule pour l'année, jusqu'au mois inclus (comme aujourd'hui), avec `Date.Month` pour distinguer le mois affiché du cumul.
2. Pour chaque trajet : `distanceArrondie = ArrondirDistance(Distance, precision) × (AllerRetour ? 2 : 1)`.
3. `totalMoisAffiche = Σ distanceArrondie` des trajets dont `Date.Month == mois` (Exigence 7.1).
4. `totalAnnuelPartiel = Σ distanceArrondie` des trajets dont `Date.Month <= mois` (Exigence 5.3, 6.2, 8.2).
5. Sélectionner la tranche via `BaremeService.CalculerMontantAsync(annee, puissanceFiscale, totalAnnuelPartiel, ...)` — **la sélection de tranche reste basée sur le cumul annuel arrondi**, conformément à l'Exigence 7.2 : « à partir du total kilométrique annuel arrondi cumulé, tout en appliquant le coefficient ainsi déterminé au seul kilométrage arrondi du mois affiché ».
6. `Montant_Previsionnel_Mensuel = Round(totalMoisAffiche × tranche.Coefficient, 2)` — **80%** en réalité `Round(totalMoisAffiche × tranche.Coefficient × 0.80m, 2)` car le coefficient de tranche seul ne représente pas le montant complet lorsqu'il existe une `Base` non nulle. Pour rester fidèle à la formule existante `CalculerMontantAsync` (`totalKm × coefficient + base`), le calcul du prévisionnel mensuel applique le barème complet à `totalMoisAffiche` (nouvel appel à `CalculerMontantAsync` avec ce total, dans la même tranche déterminée à l'étape 5) puis multiplie par 0.80.
7. `Montant_Previsionnel_Annuel = Round(bareme.Montant sur totalAnnuelPartiel × 1.00, 2)` (Exigence 8.2).
Nouveau DTO :
```csharp
public record MontantPrevisionnelDto(
int Annee,
int Mois,
int VehiculeId,
decimal TotalMoisKm, // Distance_Effective_Arrondie totale du mois affiché
decimal TotalAnnuelPartielKm, // Total_Annuel_Partiel arrondi (cumul jusqu'au mois inclus)
decimal MontantPrevisionnelMensuel, // 80% du barème appliqué à TotalMoisKm (tranche déterminée par le cumul annuel)
decimal MontantPrevisionnelAnnuel // 100% du barème appliqué à TotalAnnuelPartielKm
);
```
> **Rupture de contrat mineure** : `MontantBaremePlein` et `MontantPrevisionnel` (noms actuels) sont retirés/renommés en `MontantPrevisionnelAnnuel` et `MontantPrevisionnelMensuel`. Le frontend (`bareme.model.ts`, `PanneauTotauxComponent`, `MontantsReglesComponent`) doit être mis à jour en conséquence (voir section Frontend).
Implémentation :
```csharp
public async Task<MontantPrevisionnelDto> GetMontantPrevisionnelAsync(
int annee, int mois, 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.");
var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken);
var trajets = await _context.Trajets
.AsNoTracking()
.Where(t => t.VehiculeId == vehiculeId && t.Date.Year == annee && t.Date.Month <= mois)
.Select(t => new { t.Date.Month, t.Distance, t.AllerRetour })
.ToListAsync(cancellationToken);
decimal DistanceArrondie(decimal? distance, bool allerRetour) =>
distance.HasValue
? _precisionDistanceService.ArrondirDistance(distance.Value, precision) * (allerRetour ? 2m : 1m)
: 0m;
var totalMoisAffiche = trajets.Where(t => t.Month == mois).Sum(t => DistanceArrondie(t.Distance, t.AllerRetour));
var totalAnnuelPartiel = trajets.Sum(t => DistanceArrondie(t.Distance, t.AllerRetour));
// Tranche déterminée par le cumul annuel (Exigence 7.2) ; réutilisée pour le mois et l'année.
var baremeAnnuel = await _baremeService.CalculerMontantAsync(annee, vehicule.PuissanceFiscale, totalAnnuelPartiel, cancellationToken);
var baremeMois = await _baremeService.CalculerMontantAsync(annee, vehicule.PuissanceFiscale, totalMoisAffiche, cancellationToken);
// NB : CalculerMontantAsync sélectionne la tranche selon le total PASSÉ ; pour appliquer la
// tranche déterminée par le CUMUL au montant du MOIS, on applique manuellement le coefficient/base
// de la tranche annuelle au total du mois plutôt que de ré-sélectionner une tranche via le mois seul :
var montantMoisSelonTrancheAnnuelle = totalMoisAffiche * baremeAnnuel.Coefficient + baremeAnnuel.Base;
var montantPrevisionnelMensuel = Math.Round(montantMoisSelonTrancheAnnuelle * 0.80m, 2, MidpointRounding.AwayFromZero);
var montantPrevisionnelAnnuel = Math.Round(baremeAnnuel.Montant, 2, MidpointRounding.AwayFromZero);
return new MontantPrevisionnelDto(
annee, mois, vehiculeId,
Math.Round(totalMoisAffiche, precision, MidpointRounding.AwayFromZero),
Math.Round(totalAnnuelPartiel, precision, MidpointRounding.AwayFromZero),
montantPrevisionnelMensuel,
montantPrevisionnelAnnuel);
}
```
> **Correction de conception importante** : l'appel intermédiaire `baremeMois` (calculé mais non utilisé dans la formule finale) est retiré de l'implémentation définitive ci-dessus — seul `montantMoisSelonTrancheAnnuelle` (coefficient/base de la tranche annuelle appliqués au total du mois) sert au calcul, conformément à l'Exigence 7.2. Il est laissé en commentaire dans le pseudo-code ci-dessus uniquement pour expliciter le raisonnement ; l'implémentation réelle ne doit PAS effectuer cet appel inutile.
### Écran Frontend — `ParametresApplicationComponent`
Nouveau composant `frontend/src/app/features/parametres-application/parametres-application.component.ts` (+ `.html`, `.scss`), sur le modèle exact de `VehiculesComponent`/`BaremeComponent` (composant standalone, `ChangeDetectionStrategy` par défaut cohérente avec les composants de paramétrage existants — `ProfilComponent` n'utilise pas `OnPush`, ce composant n'en a pas besoin non plus).
```typescript
@Component({
selector: 'app-parametres-application',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './parametres-application.component.html',
styleUrl: './parametres-application.component.scss'
})
export class ParametresApplicationComponent implements OnInit {
private readonly parametresService = inject(ParametresApplicationService);
private readonly notification = inject(NotificationService);
readonly precisionSelectionnee = signal<0 | 1 | 2>(2);
readonly enregistrement = signal(false);
ngOnInit(): void {
this.precisionSelectionnee.set(this.parametresService.precisionDistance());
}
choisir(valeur: 0 | 1 | 2): void {
const precedente = this.precisionSelectionnee();
this.precisionSelectionnee.set(valeur);
this.enregistrement.set(true);
this.parametresService.definirPrecisionDistance(valeur).subscribe({
next: () => {
this.enregistrement.set(false);
this.notification.success('Précision des distances enregistrée.');
},
error: () => {
this.enregistrement.set(false);
this.precisionSelectionnee.set(precedente); // Exigence 2.5 : conserve la valeur précédente
this.notification.error("La modification n'a pas pu être enregistrée.");
}
});
}
}
```
Template : trois boutons/radios (« 0 », « 1 », « 2 ») avec le choix courant présélectionné (`precisionSelectionnee`), et un texte d'aide expliquant l'arrondi supérieur.
### Route et menu (`app.routes.ts`, `parametrage.component.html`)
`app.routes.ts` — nouvel enfant **entre** `compte` et `vehicules`, avec le même niveau de garde que `compte`/`vehicules` (`authGuard` seul, pas `profilConfigureGuard` — Exigence 2.6) :
```typescript
children: [
{ path: '', redirectTo: 'compte', pathMatch: 'full' },
{ path: 'compte', component: ProfilComponent },
{ path: 'application', component: ParametresApplicationComponent },
{ path: 'vehicules', component: VehiculesComponent },
{ path: 'bareme', component: BaremeComponent, canActivate: [profilConfigureGuard] }
]
```
`parametrage.component.html` — nouveau lien **entre** « Compte » et « Véhicules » (Exigence 2.1) :
```html
<a class="parametrage__lien" routerLink="/parametrage/application" routerLinkActive="parametrage__lien--actif">
Application
</a>
```
### Service Angular `ParametresApplicationService` (nouveau, `core/services/parametres-application.service.ts`)
Porte le **signal partagé** de précision courante, chargé une fois et mis à jour après modification — c'est le mécanisme central de propagation demandé (pas de rechargement par composant) :
```typescript
@Injectable({ providedIn: 'root' })
export class ParametresApplicationService {
private readonly http = inject(HttpClient);
private readonly baseUrl = environment.apiUrl;
/** Précision courante (0, 1 ou 2), partagée par toute l'application. Défaut 2 avant premier chargement. */
readonly precisionDistance = signal<0 | 1 | 2>(2);
/** Charge la précision courante depuis le backend et met à jour le signal partagé. */
charger(): Observable<ParametresApplication> {
return this.http.get<ParametresApplication>(`${this.baseUrl}/parametres-application`).pipe(
tap((p) => this.precisionDistance.set(p.precisionDistanceKm as 0 | 1 | 2))
);
}
/** Modifie la précision et met à jour le signal partagé en cas de succès. */
definirPrecisionDistance(valeur: 0 | 1 | 2): Observable<ParametresApplication> {
return this.http.put<ParametresApplication>(`${this.baseUrl}/parametres-application`, { precisionDistanceKm: valeur }).pipe(
tap((p) => this.precisionDistance.set(p.precisionDistanceKm as 0 | 1 | 2))
);
}
}
```
```typescript
// core/models/parametres-application.model.ts
export interface ParametresApplication {
precisionDistanceKm: number;
}
```
**Chargement initial** : suit le même schéma que le chargement du profil dans `AppComponent` (`effect()` déclenché dès que l'utilisateur authentifié quitte une page publique — cf. `AppComponent` existant qui charge déjà `ProfilService.getProfil()` à ce moment). `AppComponent` appelle également `ParametresApplicationService.charger()` à cet instant, afin que le signal soit disponible avant l'affichage des vues protégées.
### Fonction utilitaire d'arrondi frontend (`shared/utils/arrondir-distance.ts`, nouveau fichier)
```typescript
/**
* Arrondit une distance brute (km) au supérieur (ceiling) à la précision donnée.
* Miroir exact de PrecisionDistanceService.ArrondirDistance côté backend.
*/
export function arrondirDistance(distanceBrute: number, precision: 0 | 1 | 2): number {
const facteur = Math.pow(10, precision);
return Math.ceil(distanceBrute * facteur) / facteur;
}
/** Distance_Effective_Arrondie : arrondi de la distance simple PUIS doublement si aller/retour. */
export function distanceEffectiveArrondie(
distanceBrute: number | null,
allerRetour: boolean,
precision: 0 | 1 | 2
): number | null {
if (distanceBrute == null) {
return null;
}
const arrondie = arrondirDistance(distanceBrute, precision);
return allerRetour ? arrondie * 2 : arrondie;
}
```
> **Remarque sur la source de la distance brute côté frontend** : après la modification backend (Requirement 5/6 ci-dessus), les DTOs `TrajetDto` exposés par `ResumeService` contiennent déjà `Distance_Effective_Arrondie` (le backend a effectué l'arrondi avant l'envoi). Le frontend n'a donc PAS besoin de ré-arrondir la valeur reçue de l'API pour l'affichage de la liste des trajets ou des totaux — il l'affiche telle quelle. La fonction `arrondirDistance`/`distanceEffectiveArrondie` côté frontend sert uniquement dans les cas où le frontend a besoin d'un **rendu réactif immédiat sans aller-retour serveur** après un changement de précision (Exigence 2.4/5.5 : recalcul immédiat des totaux affichés) — c'est-à-dire lorsqu'un composant recalcule l'affichage à partir de distances brutes déjà en mémoire (ex. recalcul du panneau de totaux en cache local avant confirmation serveur), plutôt que de déclencher un rechargement HTTP à chaque clic sur 0/1/2.
### Composants frontend à modifier pour appliquer/afficher l'arrondi
| Composant | Fichier réel | Modification |
|---|---|---|
| `TrajetListComponent` | `frontend/src/app/features/trajet-list/trajet-list.component.ts` (méthode `distanceAffichee`) | La distance provient déjà du `TrajetDto.distance` renvoyé par le backend arrondi (Requirement 5/6) ; `distanceAffichee` n'a plus besoin d'arrondir elle-même à 2 décimales fixes — elle formate simplement selon `precisionSelectionnee()` du `ParametresApplicationService` injecté (nombre de décimales affichées cohérent avec la précision choisie, ex. `toFixed(precision)`). |
| `PanneauTotauxComponent` | `frontend/src/app/features/panneau-totaux/panneau-totaux.component.ts` + `.html` | `totaux()` provient de `TrajetService.getTotauxJour` (backend déjà arrondi) ; le pipe `number: '1.2-2'` fixe dans le template est remplacé par un format dynamique basé sur `parametresService.precisionDistance()` (ex. `'1.{p}-{p}'` construit dynamiquement, ou méthode `formatKm()` utilisant `toFixed`). Le bloc « Indication de versement (80%) » est renommé et complété avec la nouvelle ligne « Montant prévisionnel annuel » (Requirement 8), consommant les nouveaux champs `montantPrevisionnelMensuel`/`montantPrevisionnelAnnuel` du DTO. |
| `CarteTrajetsComponent` | `frontend/src/app/features/carte-trajets/carte-trajets.component.ts` (méthode `contenuInfobulle`) | `trajet.distance.toFixed(2)` est remplacé par `trajet.distance.toFixed(this.parametresService.precisionDistance())`. La distance vient de `TrajetService.getTrajetsJournee` (`TrajetCarteDto`), à corriger côté backend pour renvoyer la Distance_Effective_Arrondie (même service `ResumeService`/logique commune que la liste, ou méthode dédiée du `ITrajetService` selon l'implémentation réelle du endpoint `/api/trajets/{date}/journee` — à vérifier au moment de l'implémentation dans `ITrajetService`). |
| `MontantsReglesComponent` | `frontend/src/app/features/montants-regles/montants-regles.component.ts` | Consomme `BaremeService.getMontantPrevisionnel(annee, mois, vehiculeId)` ; le champ lu passe de `p.montantPrevisionnel` à `p.montantPrevisionnelMensuel`. Affichage inchangé (2 décimales monétaires, Exigence 8.4). |
| `BaremeService` (frontend) | `frontend/src/app/core/services/bareme.service.ts` | Le modèle `MontantPrevisionnel` (`frontend/src/app/core/models/bareme.model.ts`) est mis à jour avec les nouveaux champs (`totalMoisKm`, `totalAnnuelPartielKm`, `montantPrevisionnelMensuel`, `montantPrevisionnelAnnuel`) miroir du DTO backend. |
| `ParametrageComponent` | `frontend/src/app/features/parametrage/parametrage.component.html` | Nouveau lien « Application » (voir section Route et menu ci-dessus). |
| `AppComponent` | `frontend/src/app/app.component.ts` | Ajoute le chargement initial de `ParametresApplicationService.charger()` au même moment que le chargement du profil (cf. section Service Angular ci-dessus). |
---
## Règle de calcul Aller_Retour : ordre des opérations
**Règle (Exigence 4.2)** : la distance simple brute est arrondie au supérieur **avant** d'être doublée. Ne jamais doubler la distance brute puis arrondir le résultat — les deux ordres peuvent produire des résultats différents.
**Exemple chiffré (Precision_Distance = 0)** :
- Distance simple brute (stockée en base, déjà arrondie à 2 décimales par `ORSClient`) : `12.30 km`.
- **Ordre correct (retenu)** : `ArrondirDistance(12.30, 0) = Ceiling(12.30) = 13 km`, puis doublement : `13 × 2 = 26 km`. → **Distance_Effective_Arrondie = 26 km**.
- **Ordre incorrect (rejeté)** : doublement d'abord `12.30 × 2 = 24.60 km`, puis arrondi : `Ceiling(24.60) = 25 km`. → 25 km ≠ 26 km.
L'écart (26 vs 25) illustre pourquoi l'ordre est normatif et testé explicitement (voir Stratégie de test).
Implémentation (backend comme frontend) : toujours calculer `ArrondirDistance(distanceSimple, precision) × (allerRetour ? 2 : 1)`, jamais `ArrondirDistance(distanceSimple × (allerRetour ? 2 : 1), precision)`.
---
## Cohérence PDF / Carte (Exigence 9)
- **`PdfService`** (`backend/SchotzIadKmTracker.Api/Services/PdfService.cs`) : ne fait aucun calcul de distance brut lui-même — il compose `IResumeService.GetResumeMensuelAsync/GetResumeAnnuelAsync` (table des trajets et totaux du rapport mensuel, méthode privée `ComposerTableTrajets`) et `IRemboursementService.GetRegularisationAnnuelleAsync` (synthèse annuelle, méthode privée `ComposerTableSyntheseAnnuelle`). Une fois ces deux services corrigés pour retourner des distances déjà arrondies selon Precision_Distance (Requirement 5/6 ci-dessus), le PDF hérite automatiquement de la cohérence, **sans modification de `PdfService` lui-même**, à l'exception d'un point : `AssemblerModeleMensuelAsync` recalcule localement `totalIkMois = resumeMensuel.TotalKm × bareme.Coefficient` — ce total provient déjà de `resumeMensuel.TotalKm` (corrigé), donc cohérent sans changement de code dans `PdfService`.
- **`CarteTrajetsComponent`** (`frontend/src/app/features/carte-trajets/carte-trajets.component.ts`, méthode `contenuInfobulle`) : la ligne `trajet.distance.toFixed(2)` doit devenir `trajet.distance.toFixed(precision)` où `precision` provient de `ParametresApplicationService.precisionDistance()` injecté dans le composant. La valeur `trajet.distance` elle-même doit être la Distance_Effective_Arrondie renvoyée par le endpoint utilisé par `TrajetService.getTrajetsJournee` (à vérifier/corriger côté `ITrajetService`/contrôleur `TrajetsController` au moment de l'implémentation, par cohérence avec `ResumeService`).
- Exigence 9.4 (valeur strictement identique) est garantie par le fait qu'un seul point d'arrondi (`IPrecisionDistanceService.ArrondirDistance`) est appelé, à la même précision courante, par tous les services qui produisent une distance affichée.
---
## Écarts avec `schotziad-km-tracker/design.md` existant
Cette section documente, sans modifier le fichier `design.md` existant, les endpoints et composants dont le comportement change avec cette spec, afin qu'une mise à jour ultérieure de la spec de base en tienne compte :
| Élément existant | Comportement actuel (design.md) | Nouveau comportement (precision-distances-km) |
|---|---|---|
| `IMontantRegleService.GetMontantPrevisionnelAsync` | 80% du barème plein appliqué au **Total_Annuel_Partiel** | 80% appliqué au **seul mois affiché** (tranche déterminée par le cumul annuel) ; ajout d'un second montant 100% sur le Total_Annuel_Partiel |
| `MontantPrevisionnelDto` | Champs `TotalAnnuelPartielKm`, `MontantBaremePlein`, `MontantPrevisionnel` | Renommés/complétés : `TotalMoisKm`, `TotalAnnuelPartielKm`, `MontantPrevisionnelMensuel`, `MontantPrevisionnelAnnuel` |
| `PanneauTotauxComponent` | Affiche un unique bloc « Indication de versement (80%) » sur le Total_Annuel_Partiel | Affiche le Montant_Previsionnel_Mensuel (mois affiché) PUIS, en dessous, le Montant_Previsionnel_Annuel (100%, cumul annuel) |
| `MontantsReglesComponent` | Colonne « Montant_Previsionnel » = 80% sur cumul annuel par mois | Colonne « Montant_Previsionnel_Mensuel » = 80% sur le seul mois de la ligne |
| `ResumeService` / `TotauxService` / `RemboursementService` | Distances sommées à partir de `DistanceEffective` brute (2 décimales fixes issues d'ORS) | Distances sommées à partir de `Distance_Effective_Arrondie` (précision paramétrable, arrondi ceiling) |
| `TrajetDto.Distance` (résumés) | Distance brute effective (non arrondie au-delà des 2 décimales de calcul) | Distance_Effective_Arrondie selon Precision_Distance courante |
| `CarteTrajetsComponent` infobulle | `toFixed(2)` fixe | `toFixed(precision)` dynamique |
| `ParametrageComponent` menu | Compte, Véhicules, Barème | Compte, **Application**, Véhicules, Barème |
| Aucune table de configuration globale | — | Nouvelle table `ParametresApplication` (singleton) |
---
## Error Handling
| Cas | Origine | Traitement |
|---|---|---|
| `PrecisionDistanceKm` soumis hors {0, 1, 2} | `PrecisionDistanceService.DefinirAsync` | Lève `PrecisionDistanceInvalideException` → contrôleur retourne **400** avec message explicite (Exigence 1.6) |
| Requête sans jeton JWT valide sur `GET`/`PUT /api/parametres-application` | Filtre d'autorisation global (`Program.cs`) | **401** avant d'atteindre le contrôleur (Exigence 1.7, 2.6), comportement identique aux autres endpoints protégés |
| Échec réseau/serveur lors de l'enregistrement d'une nouvelle précision (frontend) | `ParametresApplicationComponent.choisir` | Le composant restaure `precisionSelectionnee` à la valeur précédente et affiche une notification d'erreur (Exigence 2.5), sans modifier le signal partagé (qui reste à l'ancienne valeur tant que le serveur n'a pas confirmé) |
| Véhicule introuvable dans `GetMontantPrevisionnelAsync` | `MontantRegleService` | `VehiculeNotFoundException` existante, inchangée (déjà mappée en 404 par le contrôleur existant) |
| Table `ParametresApplication` absente au premier démarrage | `PrecisionDistanceService.ObtenirAsync`/`GetOrCreateAsync` | Création à la volée de la ligne singleton avec la valeur par défaut 2, sur le modèle de `ProfilService.GetOrCreateProfilAsync` — aucune exception |
| Distance brute `null` (Trajet non calculable) rencontrée dans un calcul de précision | Tous les services consommateurs | `ArrondirDistance` n'est jamais appelé sur `null` ; la contribution du trajet à la somme reste 0, comme le comportement actuel (`Distance.GetValueOrDefault()`) |
---
## Testing Strategy
### Applicabilité du property-based testing
La fonction `ArrondirDistance` (backend) / `arrondirDistance` (frontend) est une **fonction pure** avec une propriété universelle claire (arrondi supérieur à une précision donnée) : le PBT s'applique pleinement. La règle d'ordre « arrondir puis doubler » pour Aller_Retour est également une propriété universelle testable sur toute distance et tout indicateur booléen. En revanche, l'intégration UI (sélection du menu, affichage du signal partagé, rendu Leaflet, génération PDF) relève de tests d'exemple/intégration classiques, pas de PBT (comportement UI, pas fonction pure à large espace d'entrée testable utilement en PBT).
### Prework — analyse de testabilité des critères d'acceptation
**1.1** THE Backend SHALL persister une valeur unique de Precision_Distance parmi {0,1,2}
Thoughts : comportement de persistance simple, un test d'exemple suffit (enregistrer puis relire chaque valeur valide).
Testable : yes - example
**1.2** Valeur par défaut = 2 si absente
Thoughts : cas précis unique (état initial). Exemple.
Testable : yes - example
**1.3/1.4** Exposition d'API GET/PUT protégées
Thoughts : vérifie la présence d'endpoints, pas une propriété sur un espace d'entrée. Exemple/intégration.
Testable : yes - example
**1.5** Application immédiate sans migration des données stockées
Thoughts : comportement d'absence d'effet de bord sur les données existantes. Testable par exemple (créer des trajets, changer la précision, vérifier que `Trajet.Distance` est inchangé) — cf. Property ci-dessous, généralisable en propriété d'invariance.
Testable : yes - property (invariant : changer Precision_Distance ne modifie jamais Trajet.Distance)
**1.6** Rejet si valeur ≠ {0,1,2}
Thoughts : propriété universelle sur l'espace des entiers hors {0,1,2}. Le comportement varie selon que l'entrée est dans l'ensemble valide ou non — testable par génération d'entiers aléatoires.
Testable : yes - property
**1.7** Rejet sans session valide
Thoughts : comportement d'authentification binaire, ne varie pas avec le contenu de la requête. Exemple/intégration.
Testable : yes - example
**1.8** La valeur brute de Distance n'est jamais modifiée par un changement de précision
Thoughts : invariant testable pour toute distance et toute séquence de changements de précision.
Testable : yes - property
**2.1–2.6** Sous-menu Application (positionnement, présélection, confirmation, erreur, garde d'accès)
Thoughts : comportements d'interface spécifiques à des scénarios précis, pas une propriété sur un large espace d'entrée.
Testable : yes - example
**3.1–3.5** Arrondi supérieur d'un trajet simple selon la précision (0/1/2), toujours ≥ distance brute
Thoughts : propriété universelle par excellence — pour toute distance brute positive et toute précision valide, l'arrondi ceiling est supérieur ou égal à la distance brute, et coïncide avec l'arrondi mathématique attendu à chaque précision.
Testable : yes - property
**4.1–4.3** Arrondi avant doublement pour Aller_Retour ; non-A/R = Distance_Arrondie
Thoughts : propriété universelle sur l'ordre des opérations, testable par génération de distances et comparaison des deux ordres de calcul.
Testable : yes - property
**5.1–5.4** Sommation de distances déjà arrondies, sans arrondi supplémentaire après somme
Thoughts : propriété universelle sur des listes de trajets générées aléatoirement — la somme des distances arrondies individuellement doit être exactement égale au total renvoyé (pas de second arrondi qui la modifierait).
Testable : yes - property
**5.5** Recalcul immédiat des totaux à l'affichage lors d'un changement de précision (sans rechargement de page)
Thoughts : comportement d'interface réactive (signal partagé), pas une fonction pure à espace d'entrée large. Exemple/intégration (vérifier que le signal déclenche un nouveau rendu).
Testable : yes - example
**6.1–6.6** Application du barème aux distances arrondies (mensuel, annuel, réel, frais mensuel, régularisation, sélection de tranche)
Thoughts : ce sont des compositions de la propriété d'arrondi déjà couverte (Property 1/2) avec la logique de barème déjà testée dans la spec `schotziad-km-tracker`. La nouveauté testable ici est que le total transmis au barème est le total déjà arrondi — couvert par la propriété d'agrégation (Property 3) et par le fait que `CalculerMontantAsync` reste inchangé (déjà testé ailleurs).
Testable : no (couvert par les propriétés 1–3 ; pas de nouvelle propriété distincte nécessaire)
**7.1–7.4** Montant_Previsionnel_Mensuel basé sur le seul mois affiché, tranche déterminée par le cumul annuel
Thoughts : propriété universelle — pour toute combinaison de distances mensuelles et de barème, le montant mensuel ne doit dépendre QUE des distances du mois affiché (invariance par rapport aux distances des autres mois, à tranche égale).
Testable : yes - property
**8.1–8.5** Montant_Previsionnel_Annuel = 100% sur le Total_Annuel_Partiel, affiché sous le mensuel, précision 2 décimales
Thoughts : le calcul (100% sur le cumul) est une propriété universelle (relation simple avec le montant du barème plein déjà calculé) ; l'affichage/positionnement est un exemple UI.
Testable : yes - property (relation Montant_Previsionnel_Annuel = montant plein du barème sur le cumul, sans minoration) + example (positionnement UI)
**9.1** Infobulle carte = Distance_Effective_Arrondie selon la précision courante
Thoughts : propriété universelle (la valeur affichée doit être strictement égale à la fonction d'arrondi appliquée à la distance brute et à l'indicateur A/R).
Testable : yes - property
**9.2–9.3** PDF utilise la Distance_Effective_Arrondie et la précision en vigueur au moment de la génération
Thoughts : propriété universelle sur les valeurs numériques du document généré, mais génération PDF coûteuse (QuestPDF) — préférable de tester la fonction de composition des données (modèle) plutôt que le rendu binaire complet.
Testable : yes - property (sur le modèle de données assemblé, pas sur le rendu binaire)
**9.4** Cohérence stricte entre liste, panneau, carte, PDF et barème pour un même trajet à précision égale
Thoughts : propriété universelle mais transversale à plusieurs services déjà couverts individuellement (Property 1/2/6) — se déduit de l'utilisation d'un point d'arrondi unique. Pas de nouvelle propriété distincte nécessaire, mais un test d'intégration (exemple) vérifiant l'égalité entre les sorties de `ResumeService`, `TotauxService` et `RemboursementService` pour un jeu de trajets donné apporte une garantie de non-régression bienvenue.
Testable : edge-case (test d'intégration de cohérence croisée, pas une propriété PBT distincte)
### Réflexion de redondance
- Les critères 3.1–3.5 (arrondi simple par précision) et 4.1–4.3 (arrondi puis doublement) partagent la même fonction sous-jacente : une seule **Property 1** (arrondi ceiling ≥ brute, exact par précision) et une seule **Property 2** (ordre arrondi-puis-doublement) suffisent, sans décliner une propriété par valeur de précision.
- Les critères 6.1–6.6 sont entièrement dérivés des propriétés d'arrondi (1, 2) et d'agrégation (3) : aucune propriété supplémentaire.
- Le critère 1.8 (invariance de `Trajet.Distance`) et le critère 5.1–5.4 (pas d'arrondi après somme) sont regroupés respectivement en **Property 5** et **Property 3** — pas de doublon avec 1.6 (validation stricte, **Property 4**, qui porte sur un autre aspect : le rejet des valeurs invalides, non l'arrondi lui-même).
- Le critère 9.4 (cohérence croisée) est une consé quence directe des propriétés 1–3 appliquées via un point d'arrondi unique ; il est couvert par un test d'intégration (exemple), pas par une propriété PBT distincte.
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Arrondi supérieur exact et jamais inférieur à la distance brute
*For any* distance brute positive et toute précision valide (0, 1 ou 2), `ArrondirDistance(distance, precision)` retourne une valeur supérieure ou égale à `distance`, et cette valeur, multipliée par `10^precision`, est un entier (le résultat est bien un multiple exact du pas de précision).
**Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5**
### Property 2: Arrondi avant doublement pour Aller_Retour
*For any* distance simple brute positive et toute précision valide, `distanceEffectiveArrondie(distance, allerRetour=true, precision)` est égal à `2 × ArrondirDistance(distance, precision)`, et est strictement différent, en général, de `ArrondirDistance(2 × distance, precision)` — l'implémentation ne doit jamais utiliser ce second ordre. Pour `allerRetour=false`, `distanceEffectiveArrondie` est égal à `ArrondirDistance(distance, precision)` (sans doublement).
**Validates: Requirements 4.1, 4.2, 4.3**
### Property 3: Sommation de distances déjà arrondies sans arrondi supplémentaire
*For any* liste de trajets (distance brute nullable, indicateur aller/retour), la somme des `Distance_Effective_Arrondie` de chaque trajet (calculée individuellement) est strictement égale à la valeur de total retournée par le service d'agrégation (résumé mensuel/annuel, totaux jour/mois/année, régularisation annuelle) — aucun arrondi n'est appliqué une seconde fois après la sommation.
**Validates: Requirements 5.1, 5.2, 5.3, 5.4**
### Property 4: Validation stricte de la précision
*For any* entier soumis comme nouvelle valeur de Precision_Distance, `PrecisionDistanceService.DefinirAsync` accepte et persiste la valeur si et seulement si elle appartient à {0, 1, 2} ; pour toute autre valeur, l'opération est rejetée (exception levée) et la valeur précédemment persistée reste inchangée.
**Validates: Requirements 1.6**
### Property 5: Invariance de la distance brute stockée face à un changement de précision
*For any* séquence de changements de Precision_Distance (valeurs valides), la colonne `Trajet.Distance` de tout trajet existant avant cette séquence reste strictement inchangée après celle-ci ; seule la valeur affichée/agrégée à la lecture varie.
**Validates: Requirements 1.5, 1.8**
### Property 6: Montant_Previsionnel_Mensuel dépend uniquement du mois affiché
*For any* ensemble de trajets répartis sur plusieurs mois d'une même année pour un véhicule donné, et pour tout mois M sélectionné, le Montant_Previsionnel_Mensuel calculé pour M ne change pas si l'on modifie les distances des trajets d'un autre mois que M, tant que le cumul annuel reste dans la même tranche de barème (la tranche appliquée est déterminée par le cumul, mais le montant du mois ne dépend que du kilométrage du mois M).
**Validates: Requirements 7.1, 7.2**
### Property 7: Montant_Previsionnel_Annuel égal au barème plein sur le cumul
*For any* Total_Annuel_Partiel arrondi et toute puissance fiscale/année ayant un barème configuré, le Montant_Previsionnel_Annuel est égal au montant retourné par `CalculerMontantAsync` appliqué à ce même Total_Annuel_Partiel, sans aucune minoration (coefficient 1.00), arrondi à 2 décimales.
**Validates: Requirements 8.2, 8.5**
### Property 8: Cohérence de l'infobulle carte avec la Distance_Effective_Arrondie
*For any* trajet et toute précision valide, la distance présentée dans l'infobulle de la carte est strictement égale à `distanceEffectiveArrondie(trajet.distanceBrute, trajet.allerRetour, precision)`.
**Validates: Requirements 9.1**
### Property 9: Modèle de données PDF cohérent avec les résumés/régularisations
*For any* jeu de trajets d'un mois ou d'une année, les valeurs kilométriques assemblées dans le modèle du rapport PDF (`ModeleRapportMensuel`, `ModeleRapportAnnuel`) sont strictement égales aux valeurs retournées par `IResumeService`/`IRemboursementService` pour les mêmes paramètres et la même précision courante.
**Validates: Requirements 9.2, 9.3**
### Configuration des tests de propriété
- **Bibliothèque** : côté backend (.NET), utiliser **FsCheck** (ou **CsCheck**, déjà idiomatique en C#/xUnit) — bibliothèque standard PBT pour .NET, ne pas réimplémenter un générateur maison.
- **Itérations minimales** : 100 par propriété.
- **Tag de traçabilité** dans chaque test :
`// Feature: precision-distances-km, Property {n}: {texte de la propriété}`
- Chaque propriété est implémentée par **un seul test property-based** (pas un test par précision — les générateurs couvrent {0, 1, 2} et un espace de distances flottantes/décimales positives, y compris valeurs limites proches d'un seuil d'arrondi comme `12.000...1`).
### Tests unitaires (exemples et cas limites, complémentaires aux propriétés)
- `PrecisionDistanceService.ObtenirAsync` retourne 2 par défaut quand la table est vide (Exigence 1.2).
- `PUT /api/parametres-application` avec `precisionDistanceKm = 3` retourne 400 (Exigence 1.6, cas concret).
- `GET`/`PUT /api/parametres-application` sans jeton retourne 401 (Exigence 1.7).
- Exemple chiffré Aller_Retour documenté ci-dessus (12.30 km → 26 km avec précision 0, et non 25 km) — implémenté comme test unitaire explicite en plus de la Property 2.
- `ParametresApplicationComponent.choisir` restaure la sélection précédente et notifie une erreur si l'appel HTTP échoue (Exigence 2.5).
- Positionnement du lien « Application » entre « Compte » et « Véhicules » dans `parametrage.component.html` (test de structure du template ou test e2e léger).
- `MontantsReglesComponent` affiche `montantPrevisionnelMensuel` (et non l'ancien champ) sans erreur de compilation/liaison après le renommage du DTO.
### Tests d'intégration
- Un scénario bout-en-bout (base SQLite en mémoire ou fichier temporaire) : créer des trajets sur plusieurs mois, changer Precision_Distance, vérifier que `ResumeService`, `TotauxService` et `RemboursementService` renvoient des valeurs cohérentes entre elles pour les mêmes trajets (couvre l'Exigence 9.4 en test d'intégration, cf. prework 9.4).
- Génération effective d'un PDF mensuel et annuel (au moins 1 exemple chacun) pour vérifier l'absence d'exception de rendu après les changements de modèle (pas de PBT sur le rendu binaire lui-même, conformément à la règle « ne pas faire de PBT sur des opérations coûteuses/rendu »).
@@ -0,0 +1,149 @@
# Requirements Document
## Introduction
Cette fonctionnalité introduit un paramètre global de **précision d'affichage des distances** (nombre de décimales : 0, 1 ou 2) dans l'application **SchotzIAD KM Tracker**, ainsi qu'une règle d'arrondi supérieur systématique appliquée à cette précision pour toute distance affichée ou agrégée (trajet, totaux journaliers/mensuels/annuels, carte, export PDF, barème kilométrique). Elle modifie également le mode de calcul de l'indication de versement mensuelle (Montant_Previsionnel), qui passe d'un calcul basé sur le cumul annuel partiel à un calcul basé uniquement sur le mois affiché, et introduit une nouvelle ligne d'affichage « Montant prévisionnel annuel » (100% du barème, projection sur le cumul annuel partiel).
> **Note** : Cette spec **complète** la spec existante `schotziad-km-tracker` (`requirements.md`, `design-requirements.md`, `design.md`). Elle réutilise et étend les termes déjà définis dans le glossaire de `schotziad-km-tracker` (notamment Trajet, Distance, Aller_Retour, Distance_Effective, Résumé_Mensuel, Résumé_Annuel, Total_Annuel_Partiel, Panneau_Totaux, Bareme_Kilometrique, Montant_Previsionnel, Montant_Reel, Regularisation, Frais_Reel_Mensuel, Page_Parametrage, Export_PDF, Carte). Les exigences ci-dessous priment sur celles de `schotziad-km-tracker` en cas de divergence portant sur le calcul du Montant_Previsionnel et sur la précision d'affichage des distances ; toutes les autres exigences de `schotziad-km-tracker` restent applicables sans changement.
---
## Glossary
- **Precision_Distance** : Le paramètre global de l'Application déterminant le nombre de décimales (0, 1 ou 2) utilisé pour l'arrondi supérieur des distances à l'affichage et dans les agrégats. Ce paramètre est unique pour toute l'Application (pas de déclinaison par Véhicule ni par Utilisateur).
- **Distance_Arrondie** : La valeur d'une Distance simple (non doublée) après application de l'arrondi supérieur mathématique (ceiling) à la précision définie par Precision_Distance. Exemple avec Precision_Distance = 0 : 12,3 km devient 13 km. Exemple avec Precision_Distance = 1 : 12,34 km devient 12,4 km. Exemple avec Precision_Distance = 2 : 12,347 km devient 12,35 km.
- **Distance_Effective_Arrondie** : La distance comptée d'un Trajet dans tous les agrégats et affichages une fois la règle d'arrondi appliquée : égale à la Distance_Arrondie, ou à son double lorsque le Trajet est un Aller_Retour (la Distance simple est arrondie AVANT d'être doublée, jamais après).
- **Sous_Menu_Application** : L'entrée « Application » du menu latéral de la Page_Parametrage, positionnée entre « Compte » et « Véhicules », permettant de configurer Precision_Distance.
- **Montant_Previsionnel_Mensuel** : L'indication de versement mensuelle (80%) redéfinie par la présente spec : elle correspond à 80% du Bareme_Kilometrique appliqué à la Distance_Effective_Arrondie totale du seul mois affiché (et non plus au Total_Annuel_Partiel).
- **Montant_Previsionnel_Annuel** : La nouvelle ligne d'affichage introduite par la présente spec, calculée comme 100% (sans minoration) du Bareme_Kilometrique appliqué au Total_Annuel_Partiel arrondi (cumul de l'année sélectionnée jusqu'au mois affiché inclus), affichée sous le Montant_Previsionnel_Mensuel.
---
## Requirements
### Requirement 1: Paramètre global de précision des distances
**User Story:** En tant qu'utilisateur, je veux choisir le nombre de décimales utilisé pour l'affichage des distances en kilomètres, afin d'adapter la précision affichée à mes besoins (ex. arrondi à l'unité pour une lecture rapide, ou deux décimales pour une précision fine).
#### Acceptance Criteria
1. THE Backend SHALL persister une valeur unique de Precision_Distance pour l'ensemble de l'Application, parmi exactement trois valeurs possibles : 0, 1 ou 2.
2. WHERE aucune valeur de Precision_Distance n'a encore été enregistrée, THE Backend SHALL considérer la valeur par défaut de Precision_Distance comme égale à 2.
3. THE Backend SHALL exposer une API protégée par authentification permettant de lire la valeur courante de Precision_Distance.
4. THE Backend SHALL exposer une API protégée par authentification permettant de modifier la valeur de Precision_Distance.
5. WHEN l'utilisateur soumet une nouvelle valeur de Precision_Distance égale à 0, 1 ou 2, THE Backend SHALL persister cette valeur et SHALL l'appliquer immédiatement à tous les calculs et affichages ultérieurs de distances, pour les Trajets passés comme futurs, sans migration ni recalcul des données stockées.
6. IF le Backend reçoit une valeur de Precision_Distance différente de 0, 1 ou 2, THEN THE Backend SHALL rejeter la modification et retourner un message d'erreur indiquant que seules les valeurs 0, 1 et 2 sont autorisées.
7. IF le Backend reçoit une requête de lecture ou de modification de Precision_Distance sans Session authentifiée valide, THEN THE Backend SHALL rejeter la requête.
8. THE Backend SHALL ne jamais modifier la valeur brute stockée de la Distance d'un Trajet (colonne Distance existante) ni les calculs de Calculateur_Distance (routier ou Haversine) suite à un changement de Precision_Distance ; seuls l'affichage et les agrégats calculés à la volée sont affectés.
---
### Requirement 2: Sous-menu « Application » dans le paramétrage
**User Story:** En tant qu'utilisateur, je veux accéder à un sous-menu « Application » dans la page de paramétrage, afin de configurer la précision d'affichage des distances au même endroit que mes autres réglages.
#### Acceptance Criteria
1. THE Page_Parametrage SHALL présenter, dans son menu latéral, l'entrée Sous_Menu_Application positionnée entre l'entrée « Compte » et l'entrée « Véhicules » (ordre : Compte, Application, Véhicules, Barème).
2. WHEN l'utilisateur sélectionne Sous_Menu_Application dans le menu latéral de la Page_Parametrage, THE Frontend SHALL afficher à droite du menu un contrôle de sélection « Nombre de décimales des distances (km) » proposant exactement trois choix : 0, 1 et 2, sans rechargement complet de la page.
3. WHEN la page Sous_Menu_Application est affichée, THE Frontend SHALL présélectionner le choix correspondant à la valeur courante de Precision_Distance renvoyée par le Backend.
4. WHILE la page Sous_Menu_Application est affichée, WHEN l'utilisateur sélectionne une valeur différente pour Precision_Distance, THE Frontend SHALL envoyer la nouvelle valeur au Backend et afficher un message de confirmation lorsque l'enregistrement réussit.
5. IF l'enregistrement d'une nouvelle valeur de Precision_Distance échoue au niveau du Backend, THEN THE Frontend SHALL afficher un message d'erreur indiquant que la modification n'a pas pu être enregistrée et SHALL conserver l'affichage de la valeur précédemment active.
6. THE Backend SHALL autoriser l'accès à Sous_Menu_Application indépendamment de l'état de configuration du Profil (Profil_Configure ou non), de la même manière que les entrées « Compte » et « Véhicules » de la Page_Parametrage.
---
### Requirement 3: Arrondi supérieur d'un trajet simple
**User Story:** En tant qu'utilisateur, je veux que la distance affichée d'un trajet simple soit arrondie au supérieur selon la précision choisie, afin d'obtenir un affichage cohérent et jamais sous-évalué.
#### Acceptance Criteria
1. WHEN le Frontend affiche la distance d'un Trajet qui n'est pas un Aller_Retour, THE Frontend SHALL afficher la Distance_Arrondie de ce Trajet, calculée par arrondi supérieur mathématique de la Distance brute à la précision définie par Precision_Distance.
2. WHILE Precision_Distance est égal à 0, THE Distance_Arrondie SHALL être égale à la Distance brute arrondie au kilomètre entier supérieur (ex. 12,3 km devient 13 km ; 12,0 km reste 12 km).
3. WHILE Precision_Distance est égal à 1, THE Distance_Arrondie SHALL être égale à la Distance brute arrondie au dixième de kilomètre supérieur (ex. 12,34 km devient 12,4 km).
4. WHILE Precision_Distance est égal à 2, THE Distance_Arrondie SHALL être égale à la Distance brute arrondie au centième de kilomètre supérieur (ex. 12,347 km devient 12,35 km).
5. THE Distance_Arrondie d'un Trajet SHALL toujours être supérieure ou égale à la Distance brute correspondante, quelle que soit la valeur de Precision_Distance.
---
### Requirement 4: Arrondi supérieur d'un trajet aller-retour
**User Story:** En tant qu'utilisateur, je veux que la distance affichée d'un trajet aller-retour soit le double de la distance simple déjà arrondie, afin que l'ordre des opérations (arrondi puis doublement) soit cohérent et prévisible.
#### Acceptance Criteria
1. WHEN le Frontend affiche la distance d'un Trajet dont Aller_Retour est vrai, THE Frontend SHALL afficher la Distance_Effective_Arrondie égale au double de la Distance_Arrondie de la distance simple mesurée entre le départ et l'arrivée.
2. THE Backend SHALL calculer la Distance_Effective_Arrondie d'un Trajet Aller_Retour en appliquant l'arrondi supérieur à la distance simple AVANT de la multiplier par deux, et jamais en doublant d'abord la distance brute puis en arrondissant le résultat.
3. THE Distance_Effective_Arrondie d'un Trajet non-Aller_Retour SHALL être égale à sa Distance_Arrondie (sans doublement).
---
### Requirement 5: Agrégation des totaux à partir des distances arrondies
**User Story:** En tant qu'utilisateur, je veux que les totaux du jour, du mois et de l'année soient calculés en additionnant des distances déjà arrondies, afin que la somme affichée soit cohérente avec la somme visuelle des lignes de détail.
#### Acceptance Criteria
1. WHEN le Backend ou le Frontend calcule le total kilométrique d'une journée, THE Backend ou THE Frontend SHALL additionner la Distance_Effective_Arrondie de chaque Trajet de cette journée, sans appliquer d'arrondi supplémentaire après la somme.
2. WHEN le Backend calcule le total kilométrique d'un Résumé_Mensuel, THE Backend SHALL additionner la Distance_Effective_Arrondie de chaque Trajet du mois concerné, sans appliquer d'arrondi supplémentaire après la somme.
3. WHEN le Backend calcule le total kilométrique d'un Résumé_Annuel ou le Total_Annuel_Partiel, THE Backend SHALL additionner la Distance_Effective_Arrondie de chaque Trajet des mois concernés, sans appliquer d'arrondi supplémentaire après la somme.
4. THE Panneau_Totaux SHALL afficher le total du jour, le total du mois sélectionné et le Total_Annuel_Partiel de l'année sélectionnée en appliquant la règle de sommation des distances déjà arrondies définie par la présente exigence.
5. WHEN Precision_Distance est modifié, THE Frontend SHALL recalculer et afficher immédiatement les totaux du jour, du mois et de l'année selon la nouvelle valeur de Precision_Distance, sans rechargement complet de la page.
---
### Requirement 6: Application de l'arrondi au barème kilométrique
**User Story:** En tant qu'utilisateur, je veux que les calculs de barème kilométrique (indication de versement, montant réel, régularisation) utilisent les distances arrondies affichées, afin que les montants calculés correspondent aux kilomètres que je vois à l'écran.
#### Acceptance Criteria
1. WHEN le Backend calcule le Montant_Previsionnel_Mensuel, THE Backend SHALL appliquer le Bareme_Kilometrique à la somme des Distance_Effective_Arrondie des Trajets du mois affiché, et non à la Distance brute.
2. WHEN le Backend calcule le Montant_Previsionnel_Annuel, THE Backend SHALL appliquer le Bareme_Kilometrique à la valeur arrondie du Total_Annuel_Partiel telle que définie par l'Exigence 5.
3. WHEN le Backend calcule le Montant_Reel annuel d'un Véhicule, THE Backend SHALL appliquer le Bareme_Kilometrique plein (100%) au total kilométrique annuel obtenu par sommation des Distance_Effective_Arrondie de tous les Trajets de l'année, et non des Distances brutes.
4. WHEN le Backend calcule le Frais_Reel_Mensuel de chaque mois, THE Backend SHALL multiplier la somme des Distance_Effective_Arrondie des Trajets du mois par le coefficient de la tranche annuelle applicable.
5. WHEN le Backend calcule la Regularisation d'un Véhicule pour une année, THE Backend SHALL utiliser le Montant_Reel annuel calculé à partir des distances arrondies conformément à la présente exigence.
6. THE Backend SHALL déterminer la Tranche_Bareme applicable (mensuelle ou annuelle) à partir du total kilométrique arrondi correspondant, et non à partir du total kilométrique brut.
---
### Requirement 7: Indication de versement mensuelle recentrée sur le mois affiché
**User Story:** En tant qu'utilisateur, je veux que l'indication de versement (80%) corresponde uniquement au mois que je consulte, afin de mieux estimer le montant à verser pour ce mois précis plutôt que pour tout le cumul annuel.
#### Acceptance Criteria
1. WHEN l'utilisateur consulte les données d'un mois, THE Frontend SHALL afficher le Montant_Previsionnel_Mensuel calculé comme 80% du Bareme_Kilometrique appliqué à la Distance_Effective_Arrondie totale du seul mois affiché, sans y inclure les distances des mois précédents de l'année.
2. THE Backend SHALL déterminer la Tranche_Bareme utilisée pour le Montant_Previsionnel_Mensuel à partir du total kilométrique annuel arrondi cumulé (Total_Annuel_Partiel), conformément à la règle de sélection de tranche du Bareme_Kilometrique définie dans `schotziad-km-tracker` (Requirement 13), tout en appliquant le coefficient ainsi déterminé au seul kilométrage arrondi du mois affiché.
3. THE Frontend SHALL continuer à indiquer explicitement que le Montant_Previsionnel_Mensuel est une simple indication de versement et que le montant réellement versé est celui saisi par l'utilisateur (Montant_Regle).
4. THE présente exigence SHALL remplacer, pour le calcul du Montant_Previsionnel mensuel, la règle de calcul antérieure basée sur le Total_Annuel_Partiel définie dans `schotziad-km-tracker` (Requirement 13, critère 5).
---
### Requirement 8: Montant prévisionnel annuel
**User Story:** En tant qu'utilisateur, je veux voir, en dessous de l'indication de versement mensuelle, une projection du montant prévisionnel annuel sans minoration, afin d'avoir une vision du montant total attendu sur l'année en cours.
#### Acceptance Criteria
1. WHEN l'utilisateur consulte les données d'un mois, THE Frontend SHALL afficher, immédiatement sous le Montant_Previsionnel_Mensuel, une ligne « Montant prévisionnel annuel » présentant le Montant_Previsionnel_Annuel.
2. THE Backend SHALL calculer le Montant_Previsionnel_Annuel en appliquant le Bareme_Kilometrique sans minoration (100%) au Total_Annuel_Partiel arrondi de l'année sélectionnée, cumulé jusqu'au mois affiché inclus.
3. THE Frontend SHALL indiquer explicitement que le Montant_Previsionnel_Annuel est une projection indicative sans minoration (100% du barème) et qu'il ne se substitue pas au Montant_Reel calculé en fin d'année sur la base du kilométrage annuel complet.
4. THE Frontend SHALL afficher le Montant_Previsionnel_Annuel avec une précision de deux décimales, selon les mêmes conventions d'affichage monétaire que le Montant_Previsionnel_Mensuel.
5. THE Backend SHALL déterminer la Tranche_Bareme utilisée pour le Montant_Previsionnel_Annuel à partir du même total kilométrique arrondi (Total_Annuel_Partiel) auquel le barème est appliqué.
---
### Requirement 9: Cohérence de l'arrondi sur la carte et dans l'export PDF
**User Story:** En tant qu'utilisateur, je veux que la carte et les exports PDF affichent la même distance arrondie que la liste des trajets et les totaux, afin de ne jamais voir de chiffres contradictoires selon l'endroit de l'application consulté.
#### Acceptance Criteria
1. WHEN l'utilisateur survole ou clique sur un tracé de Trajet sur la Carte, THE Frontend SHALL afficher dans l'infobulle la Distance_Effective_Arrondie de ce Trajet, calculée selon la valeur courante de Precision_Distance.
2. WHEN le Backend génère un Export_PDF (mensuel ou annuel), THE Backend SHALL utiliser la Distance_Effective_Arrondie de chaque Trajet, ainsi que les totaux calculés à partir de ces distances arrondies conformément à l'Exigence 5, pour toutes les valeurs kilométriques figurant dans le document.
3. WHEN le Backend génère un Export_PDF, THE Backend SHALL appliquer la valeur de Precision_Distance en vigueur au moment de la génération du document pour arrondir les distances affichées dans le PDF.
4. THE Backend SHALL garantir que la Distance_Effective_Arrondie utilisée pour un même Trajet est strictement identique, à valeur de Precision_Distance égale, dans la liste des Trajets, le Panneau_Totaux, la Carte, l'Export_PDF et les calculs de Bareme_Kilometrique.
---
+196
View File
@@ -0,0 +1,196 @@
# Implementation Plan: Précision des distances (km)
## Overview
Cette implémentation ajoute un paramètre global `Precision_Distance` (0/1/2 décimales) avec arrondi supérieur (ceiling) systématique côté backend et frontend, refond le calcul du Montant_Previsionnel (mensuel recentré sur le mois affiché + nouveau montant annuel 100%), et propage la cohérence de l'arrondi à la liste des trajets, aux totaux, à la carte, à l'export PDF et au barème kilométrique.
L'ordre des tâches suit les dépendances réelles : entité + migration → service de précision pur → intégration dans les services d'agrégation existants → refonte du calcul prévisionnel → cohérence carte/PDF → tests backend → couche frontend (service/util → composant/route → consommateurs) → vérification finale.
## Tasks
- [x] 1. Backend — Entité `ParametresApplication` et migration EF Core
- Créer `Models/Entities/ParametresApplication.cs` (singleton `Id = 1`, `PrecisionDistanceKm` défaut 2, `UpdatedAt`)
- Ajouter `DbSet<ParametresApplication> ParametresApplication` et la configuration `OnModelCreating` (contrainte CHECK `IN (0,1,2)`, clé `ValueGeneratedNever()`, index unique singleton) dans `Data/AppDbContext.cs`
- Générer la migration EF Core (`dotnet ef migrations add AjoutParametresApplication`) créant la table avec la contrainte CHECK
- _Requirements: 1.1, 1.2_
- [x] 2. Backend — Service de précision et exception métier
- [x] 2.1 Créer l'exception `Services/Exceptions/PrecisionDistanceInvalideException.cs` (même modèle que `MontantRegleNegatifException`)
- _Requirements: 1.6_
- [x] 2.2 Créer `Services/IPrecisionDistanceService.cs` (`ObtenirAsync`, `DefinirAsync`, `ArrondirDistance`)
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.6, 1.8, 3.1, 3.2, 3.3, 3.4, 3.5, 4.2_
- [x] 2.3 Implémenter `Services/PrecisionDistanceService.cs`
- `GetOrCreateAsync` privé créant la ligne singleton par défaut (2) si absente
- `ObtenirAsync`/`DefinirAsync` avec validation stricte `{0,1,2}` levant `PrecisionDistanceInvalideException`
- `ArrondirDistance` : `Ceiling(distance × 10^precision) / 10^precision`
- _Requirements: 1.1, 1.2, 1.5, 1.6, 1.8, 3.1, 3.2, 3.3, 3.4, 3.5_
- [x]* 2.4 Écrire les tests de propriété pour `PrecisionDistanceService`
- **Property 1: Arrondi supérieur exact et jamais inférieur à la distance brute** — _Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5_
- **Property 4: Validation stricte de la précision** — _Validates: Requirements 1.6_
- **Property 5: Invariance de la distance brute stockée face à un changement de précision** — _Validates: Requirements 1.5, 1.8_
- [ ]* 2.5 Écrire les tests unitaires `PrecisionDistanceService`
- `ObtenirAsync` retourne 2 par défaut quand la table est vide (Exigence 1.2)
- `DefinirAsync(3)` lève `PrecisionDistanceInvalideException` (Exigence 1.6, cas concret)
- _Requirements: 1.2, 1.6_
- [x] 3. Backend — DTOs et contrôleur `ParametresApplicationController`
- [x] 3.1 Créer `Models/DTOs/ParametresApplicationDtos.cs` (`ParametresApplicationDto`, `SavePrecisionDistanceDto`)
- _Requirements: 1.3, 1.4_
- [x] 3.2 Créer `Controllers/ParametresApplicationController.cs` (GET/PUT `api/parametres-application`, mapping `PrecisionDistanceInvalideException` → 400)
- _Requirements: 1.3, 1.4, 1.6, 1.7_
- [x] 3.3 Enregistrer `IPrecisionDistanceService`/`PrecisionDistanceService` dans `Program.cs` (`AddScoped`, section services métier)
- _Requirements: 1.1, 1.3, 1.4_
- [ ]* 3.4 Écrire les tests d'intégration du contrôleur
- `PUT /api/parametres-application` avec `precisionDistanceKm = 3` retourne 400 (Exigence 1.6)
- `GET`/`PUT /api/parametres-application` sans jeton retourne 401 (Exigence 1.7)
- _Requirements: 1.3, 1.4, 1.6, 1.7_
- [x] 4. Checkpoint — Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
- [x] 5. Backend — Intégration de l'arrondi dans les services d'agrégation existants
- [x] 5.1 Modifier `Services/ResumeService.cs` : injecter `IPrecisionDistanceService`, appliquer `ArrondirDistance(Distance, precision) × (AllerRetour ? 2 : 1)` par trajet dans `ConstruireResumeMensuel`, exposer `TrajetDto.Distance` déjà arrondi, sommer les valeurs déjà arrondies pour `TotalKm` sans second arrondi
- _Requirements: 4.1, 4.2, 4.3, 5.2, 5.3_
- [x] 5.2 Modifier `Services/TotauxService.cs` : injecter `IPrecisionDistanceService`, remplacer la fonction locale `Effective(distance, allerRetour)` par l'arrondi avant doublement, supprimer le `Math.Round(..., 2, ...)` final existant sur `TotalJour`/`TotalMois`/`TotalAnnuelPartiel`
- _Requirements: 4.1, 4.2, 4.3, 5.1, 5.4_
- [x] 5.3 Modifier `Services/RemboursementService.cs` : injecter `IPrecisionDistanceService`, construire `kmMoisMap` à partir des distances arrondies par trajet (au lieu de la distance brute), garantissant `totalKmAnnuel` et chaque `KmMois` comme sommes de distances déjà arrondies
- _Requirements: 4.1, 4.2, 4.3, 6.3, 6.4, 6.6_
- [x]* 5.4 Écrire le test de propriété pour la sommation
- **Property 2: Arrondi avant doublement pour Aller_Retour** — _Validates: Requirements 4.1, 4.2, 4.3_
- **Property 3: Sommation de distances déjà arrondies sans arrondi supplémentaire** — _Validates: Requirements 5.1, 5.2, 5.3, 5.4_
- [x]* 5.5 Écrire le test unitaire de l'exemple chiffré Aller_Retour
- Cas 12.30 km, précision 0 → 26 km (et non 25 km), en plus de la Property 2
- _Requirements: 4.2_
- [x] 6. Backend — Refonte de `MontantRegleService.GetMontantPrevisionnelAsync`
- [x] 6.1 Créer/modifier le DTO `MontantPrevisionnelDto` (`TotalMoisKm`, `TotalAnnuelPartielKm`, `MontantPrevisionnelMensuel`, `MontantPrevisionnelAnnuel`) dans `Models/DTOs/`
- _Requirements: 6.1, 6.2, 7.1, 8.1, 8.2, 8.4_
- [x] 6.2 Modifier `Services/IMontantRegleService.cs` et `Services/MontantRegleService.cs` : injecter `IPrecisionDistanceService`, calculer `totalMoisAffiche`/`totalAnnuelPartiel` à partir des distances arrondies, déterminer la tranche via le cumul annuel (`baremeAnnuel`), appliquer son coefficient/base au total du mois pour `MontantPrevisionnelMensuel` (80%), calculer `MontantPrevisionnelAnnuel` (100% sur le cumul)
- _Requirements: 6.1, 6.2, 6.6, 7.1, 7.2, 7.4, 8.1, 8.2, 8.5_
- [x]* 6.3 Écrire les tests de propriété pour le calcul prévisionnel
- **Property 6: Montant_Previsionnel_Mensuel dépend uniquement du mois affiché** — _Validates: Requirements 7.1, 7.2_
- **Property 7: Montant_Previsionnel_Annuel égal au barème plein sur le cumul** — _Validates: Requirements 8.2, 8.5_
- [ ]* 6.4 Écrire les tests unitaires de `GetMontantPrevisionnelAsync`
- Véhicule introuvable → `VehiculeNotFoundException` (comportement existant inchangé)
- Cas limite : trajets avec distance `null` ignorés dans la somme
- _Requirements: 6.1, 6.2, 7.1, 8.1, 8.2_
- [x] 7. Backend — Cohérence carte et export PDF
- [x] 7.1 Vérifier/corriger l'endpoint trajets/journée (`ITrajetService`/`TrajetsController`, méthode consommée par `TrajetService.getTrajetsJournee`) pour renvoyer la Distance_Effective_Arrondie via `IPrecisionDistanceService`
- _Requirements: 9.1, 9.4_
- [x] 7.2 Vérifier `Services/PdfService.cs` (`AssemblerModeleMensuelAsync`, composition des tables mensuelle/annuelle) : confirmer qu'aucun recalcul de distance brute ne subsiste et que le modèle assemblé provient intégralement de `IResumeService`/`IRemboursementService` déjà corrigés
- _Requirements: 9.2, 9.3_
- [ ]* 7.3 Écrire les tests de propriété de cohérence carte/PDF
- **Property 8: Cohérence de l'infobulle carte avec la Distance_Effective_Arrondie** — _Validates: Requirements 9.1_
- **Property 9: Modèle de données PDF cohérent avec les résumés/régularisations** — _Validates: Requirements 9.2, 9.3_
- [ ]* 7.4 Écrire le test d'intégration de cohérence croisée
- Scénario bout-en-bout (SQLite en mémoire) : créer des trajets sur plusieurs mois, changer Precision_Distance, vérifier l'égalité des valeurs entre `ResumeService`, `TotauxService` et `RemboursementService` (Exigence 9.4)
- _Requirements: 9.4_
- [ ]* 7.5 Écrire les tests d'intégration de génération PDF
- Génération effective d'un PDF mensuel et d'un PDF annuel (1 exemple chacun), vérifier l'absence d'exception après les changements de modèle
- _Requirements: 9.2, 9.3_
- [x] 8. Checkpoint — Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
- [x] 9. Frontend — Modèle, service et utilitaire d'arrondi
- [x] 9.1 Créer `core/models/parametres-application.model.ts` (`ParametresApplication { precisionDistanceKm: number }`)
- _Requirements: 1.3, 1.4_
- [x] 9.2 Créer `core/services/parametres-application.service.ts` (`ParametresApplicationService` : signal partagé `precisionDistance`, `charger()`, `definirPrecisionDistance()`)
- _Requirements: 1.3, 1.4, 2.3, 2.4, 2.5_
- [x] 9.3 Créer `shared/utils/arrondir-distance.ts` (`arrondirDistance`, `distanceEffectiveArrondie`), miroir exact de la formule backend
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 4.1, 4.2, 4.3, 5.5_
- [ ]* 9.4 Écrire les tests unitaires de `arrondirDistance`/`distanceEffectiveArrondie`
- Cas 12.30 km, précision 0, aller-retour → 26 km (miroir du test backend)
- Cas limites par précision (0/1/2) et valeurs proches d'un seuil d'arrondi
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 4.1, 4.2, 4.3_
- [x]* 9.5 Mettre à jour `core/models/bareme.model.ts` et `core/services/bareme.service.ts` avec les nouveaux champs `MontantPrevisionnel` (`totalMoisKm`, `totalAnnuelPartielKm`, `montantPrevisionnelMensuel`, `montantPrevisionnelAnnuel`)
- _Requirements: 6.1, 7.1, 8.1, 8.2, 8.4_
- [x] 10. Frontend — Composant `ParametresApplicationComponent` et route/menu
- [x] 10.1 Créer `features/parametres-application/parametres-application.component.ts` (+ `.html`, `.scss`) : sélection 0/1/2, présélection de la valeur courante, confirmation de succès, restauration + notification d'erreur en cas d'échec
- _Requirements: 2.2, 2.3, 2.4, 2.5_
- [x] 10.2 Ajouter la route `/parametrage/application` dans `app.routes.ts`, entre `compte` et `vehicules`, gardée uniquement par `authGuard` (pas `profilConfigureGuard`)
- _Requirements: 2.1, 2.6_
- [x] 10.3 Ajouter le lien « Application » entre « Compte » et « Véhicules » dans `features/parametrage/parametrage.component.html`
- _Requirements: 2.1_
- [ ]* 10.4 Écrire les tests unitaires du composant `ParametresApplicationComponent`
- Présélection de la valeur courante au chargement (Exigence 2.3)
- `choisir()` restaure la sélection précédente et notifie une erreur si l'appel HTTP échoue (Exigence 2.5)
- _Requirements: 2.3, 2.4, 2.5_
- [ ]* 10.5 Écrire le test de structure du menu de paramétrage
- Vérifier le positionnement du lien « Application » entre « Compte » et « Véhicules »
- _Requirements: 2.1_
- [x] 11. Frontend — Chargement initial dans `AppComponent`
- [x] 11.1 Modifier `app.component.ts` pour appeler `ParametresApplicationService.charger()` au même moment que le chargement du profil (effect déclenché à l'authentification)
- _Requirements: 1.3, 5.5_
- [x] 12. Checkpoint — Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
- [x] 13. Frontend — Adaptation des composants consommateurs
- [x] 13.1 Modifier `features/trajet-list/trajet-list.component.ts` (méthode `distanceAffichee`) : formater selon `ParametresApplicationService.precisionDistance()` au lieu d'un arrondi fixe à 2 décimales
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
- [x] 13.2 Modifier `features/panneau-totaux/panneau-totaux.component.ts` + `.html` : format dynamique des distances basé sur `precisionDistance()`, ajout de la ligne « Montant prévisionnel annuel » sous l'indication de versement mensuelle, consommation de `montantPrevisionnelMensuel`/`montantPrevisionnelAnnuel`
- _Requirements: 5.4, 5.5, 8.1, 8.3, 8.4_
- [x] 13.3 Modifier `features/carte-trajets/carte-trajets.component.ts` (méthode `contenuInfobulle`) : `toFixed(precision)` dynamique basé sur `ParametresApplicationService.precisionDistance()`
- _Requirements: 9.1_
- [x] 13.4 Modifier `features/montants-regles/montants-regles.component.ts` : lire `montantPrevisionnelMensuel` au lieu de `montantPrevisionnel`
- _Requirements: 7.1, 7.3_
- [ ]* 13.5 Écrire les tests unitaires des composants adaptés
- `TrajetListComponent.distanceAffichee` formate selon la précision courante
- `PanneauTotauxComponent` affiche la nouvelle ligne « Montant prévisionnel annuel »
- `CarteTrajetsComponent.contenuInfobulle` utilise la précision courante
- `MontantsReglesComponent` affiche `montantPrevisionnelMensuel` sans erreur de liaison après le renommage du DTO
- _Requirements: 3.1, 5.4, 7.1, 7.3, 8.1, 8.3, 8.4, 9.1_
- [x] 14. Checkpoint final — Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
- [x] 15. Vérification finale
- [x] 15.1 Exécuter le build backend (`dotnet build`) et le build frontend (`ng build` / `npm run build`), corriger toute erreur de compilation résultant des changements de DTO/modèle
- _Requirements: 1.1–9.4 (non-régression globale)_
- [x] 15.2 Exécuter l'ensemble des tests backend (`dotnet test`) et frontend (`ng test` / `vitest --run`), vérifier que toutes les propriétés et tous les tests unitaires/intégration passent
- _Requirements: 1.1–9.4 (non-régression globale)_
## Notes
- Les tâches marquées `*` sont optionnelles (tests) et peuvent être ignorées pour un MVP plus rapide, mais restent recommandées vu la présence de 9 Correctness Properties dans le design.
- Chaque tâche référence les exigences granulaires concernées pour la traçabilité.
- Les checkpoints garantissent une validation incrémentale avant de passer à la couche suivante (backend cœur → calcul prévisionnel → cohérence carte/PDF → frontend).
- Les tests de propriété utilisent FsCheck (déjà présent dans `SchotzIadKmTracker.Tests.csproj`), avec au moins 100 itérations par propriété et le tag de traçabilité `// Feature: precision-distances-km, Property {n}: {texte}`.
- La tâche 6.2 constitue une rupture de contrat mineure sur `MontantPrevisionnelDto` : les tâches frontend 9.5 et 13.4 doivent être réalisées après la tâche 6.1 pour rester synchronisées avec les nouveaux noms de champs.
## Task Dependency Graph
```json
{
"waves": [
{ "id": 0, "tasks": ["1"] },
{ "id": 1, "tasks": ["2.1"] },
{ "id": 2, "tasks": ["2.2"] },
{ "id": 3, "tasks": ["2.3"] },
{ "id": 4, "tasks": ["2.4", "2.5", "3.1"] },
{ "id": 5, "tasks": ["3.2"] },
{ "id": 6, "tasks": ["3.3"] },
{ "id": 7, "tasks": ["3.4"] },
{ "id": 8, "tasks": ["5.1", "5.2", "5.3"] },
{ "id": 9, "tasks": ["5.4", "5.5", "6.1"] },
{ "id": 10, "tasks": ["6.2"] },
{ "id": 11, "tasks": ["6.3", "6.4", "7.1"] },
{ "id": 12, "tasks": ["7.2"] },
{ "id": 13, "tasks": ["7.3", "7.4", "7.5"] },
{ "id": 14, "tasks": ["9.1"] },
{ "id": 15, "tasks": ["9.2"] },
{ "id": 16, "tasks": ["9.3", "9.5"] },
{ "id": 17, "tasks": ["9.4", "10.1"] },
{ "id": 18, "tasks": ["10.2", "10.3"] },
{ "id": 19, "tasks": ["10.4", "10.5", "11.1"] },
{ "id": 20, "tasks": ["13.1", "13.2", "13.3", "13.4"] },
{ "id": 21, "tasks": ["13.5"] },
{ "id": 22, "tasks": ["15.1"] },
{ "id": 23, "tasks": ["15.2"] }
]
}
```
@@ -0,0 +1,637 @@
{
"pbtResults": {},
"executionHistory": {
"1. Backend — Entité `ParametresApplication` et migration EF Core": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324603235
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324614373
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324711242
}
],
"2.1 Créer l'exception `Services/Exceptions/PrecisionDistanceInvalideException.cs` (même modèle que `MontantRegleNegatifException`)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324603514
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324715209
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324763012
}
],
"2.2 Créer `Services/IPrecisionDistanceService.cs` (`ObtenirAsync`, `DefinirAsync`, `ArrondirDistance`)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324603633
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324767607
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324819140
}
],
"2.3 Implémenter `Services/PrecisionDistanceService.cs`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324603727
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324824384
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324882607
}
],
"3.1 Créer `Models/DTOs/ParametresApplicationDtos.cs` (`ParametresApplicationDto`, `SavePrecisionDistanceDto`)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324603798
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324892736
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324931371
}
],
"3.2 Créer `Controllers/ParametresApplicationController.cs` (GET/PUT `api/parametres-application`, mapping `PrecisionDistanceInvalideException` → 400)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324603868
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324949397
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325012025
}
],
"3.3 Enregistrer `IPrecisionDistanceService`/`PrecisionDistanceService` dans `Program.cs` (`AddScoped`, section services métier)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324603948
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325016241
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325065910
}
],
"4. Checkpoint — Ensure all tests pass": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604002
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325074171
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325267173
}
],
"5.1 Modifier `Services/ResumeService.cs` : injecter `IPrecisionDistanceService`, appliquer `ArrondirDistance(Distance, precision) × (AllerRetour ? 2 : 1)` par trajet dans `ConstruireResumeMensuel`, exposer `TrajetDto.Distance` déjà arrondi, sommer les valeurs déjà arrondies pour `TotalKm` sans second arrondi": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604065
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325283389
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325405943
}
],
"5.2 Modifier `Services/TotauxService.cs` : injecter `IPrecisionDistanceService`, remplacer la fonction locale `Effective(distance, allerRetour)` par l'arrondi avant doublement, supprimer le `Math.Round(..., 2, ...)` final existant sur `TotalJour`/`TotalMois`/`TotalAnnuelPartiel`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604119
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325285149
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325407417
}
],
"5.3 Modifier `Services/RemboursementService.cs` : injecter `IPrecisionDistanceService`, construire `kmMoisMap` à partir des distances arrondies par trajet (au lieu de la distance brute), garantissant `totalKmAnnuel` et chaque `KmMois` comme sommes de distances déjà arrondies": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604176
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325286774
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325409159
}
],
"6.1 Créer/modifier le DTO `MontantPrevisionnelDto` (`TotalMoisKm`, `TotalAnnuelPartielKm`, `MontantPrevisionnelMensuel`, `MontantPrevisionnelAnnuel`) dans `Models/DTOs/`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604233
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325415845
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325475007
}
],
"6.2 Modifier `Services/IMontantRegleService.cs` et `Services/MontantRegleService.cs` : injecter `IPrecisionDistanceService`, calculer `totalMoisAffiche`/`totalAnnuelPartiel` à partir des distances arrondies, déterminer la tranche via le cumul annuel (`baremeAnnuel`), appliquer son coefficient/base au total du mois pour `MontantPrevisionnelMensuel` (80%), calculer `MontantPrevisionnelAnnuel` (100% sur le cumul)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604288
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325477020
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325597163
}
],
"7.1 Vérifier/corriger l'endpoint trajets/journée (`ITrajetService`/`TrajetsController`, méthode consommée par `TrajetService.getTrajetsJournee`) pour renvoyer la Distance_Effective_Arrondie via `IPrecisionDistanceService`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604402
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325603594
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325715537
}
],
"7.2 Vérifier `Services/PdfService.cs` (`AssemblerModeleMensuelAsync`, composition des tables mensuelle/annuelle) : confirmer qu'aucun recalcul de distance brute ne subsiste et que le modèle assemblé provient intégralement de `IResumeService`/`IRemboursementService` déjà corrigés": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604453
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325605135
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325717074
}
],
"8. Checkpoint — Ensure all tests pass": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604508
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325722240
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790326716373
}
],
"9.1 Créer `core/models/parametres-application.model.ts` (`ParametresApplication { precisionDistanceKm: number }`)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604565
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790326726636
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790326790793
}
],
"9.2 Créer `core/services/parametres-application.service.ts` (`ParametresApplicationService` : signal partagé `precisionDistance`, `charger()`, `definirPrecisionDistance()`)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604619
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790326796128
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790326877000
}
],
"9.3 Créer `shared/utils/arrondir-distance.ts` (`arrondirDistance`, `distanceEffectiveArrondie`), miroir exact de la formule backend": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604682
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790326882045
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790326945574
}
],
"10.1 Créer `features/parametres-application/parametres-application.component.ts` (+ `.html`, `.scss`) : sélection 0/1/2, présélection de la valeur courante, confirmation de succès, restauration + notification d'erreur en cas d'échec": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604751
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790326959853
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327069685
}
],
"10.2 Ajouter la route `/parametrage/application` dans `app.routes.ts`, entre `compte` et `vehicules`, gardée uniquement par `authGuard` (pas `profilConfigureGuard`)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604815
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327075529
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327196670
}
],
"10.3 Ajouter le lien « Application » entre « Compte » et « Véhicules » dans `features/parametrage/parametrage.component.html`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604873
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327076628
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327197485
}
],
"11.1 Modifier `app.component.ts` pour appeler `ParametresApplicationService.charger()` au même moment que le chargement du profil (effect déclenché à l'authentification)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324604952
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327204036
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327280212
}
],
"12. Checkpoint — Ensure all tests pass": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324605014
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327287107
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327678359
}
],
"13.1 Modifier `features/trajet-list/trajet-list.component.ts` (méthode `distanceAffichee`) : formater selon `ParametresApplicationService.precisionDistance()` au lieu d'un arrondi fixe à 2 décimales": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324605079
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327683970
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327885223
}
],
"13.2 Modifier `features/panneau-totaux/panneau-totaux.component.ts` + `.html` : format dynamique des distances basé sur `precisionDistance()`, ajout de la ligne « Montant prévisionnel annuel » sous l'indication de versement mensuelle, consommation de `montantPrevisionnelMensuel`/`montantPrevisionnelAnnuel`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324605136
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327686156
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327882270
}
],
"13.3 Modifier `features/carte-trajets/carte-trajets.component.ts` (méthode `contenuInfobulle`) : `toFixed(precision)` dynamique basé sur `ParametresApplicationService.precisionDistance()`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324605196
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327687238
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327880538
}
],
"13.4 Modifier `features/montants-regles/montants-regles.component.ts` : lire `montantPrevisionnelMensuel` au lieu de `montantPrevisionnel`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324605263
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327688307
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327883770
}
],
"14. Checkpoint final — Ensure all tests pass": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324605326
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327893936
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790328182621
}
],
"15.1 Exécuter le build backend (`dotnet build`) et le build frontend (`ng build` / `npm run build`), corriger toute erreur de compilation résultant des changements de DTO/modèle": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324605386
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790328189219
},
{
"executionId": "7dd406e6-29f8-4020-98c1-c18461c20a8a",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790329414209
}
],
"15.2 Exécuter l'ensemble des tests backend (`dotnet test`) et frontend (`ng test` / `vitest --run`), vérifier que toutes les propriétés et tous les tests unitaires/intégration passent": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324605452
},
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790328190132
},
{
"executionId": "7dd406e6-29f8-4020-98c1-c18461c20a8a",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790329415260
}
],
"2. Backend — Service de précision et exception métier": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790324882738
}
],
"3. Backend — DTOs et contrôleur `ParametresApplicationController`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325065987
}
],
"5. Backend — Intégration de l'arrondi dans les services d'agrégation existants": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325409224
}
],
"6. Backend — Refonte de `MontantRegleService.GetMontantPrevisionnelAsync`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325597233
}
],
"7. Backend — Cohérence carte et export PDF": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325717153
}
],
"2.4 Écrire les tests de propriété pour `PrecisionDistanceService`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325995274
}
],
"5.4 Écrire le test de propriété pour la sommation": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325995448
}
],
"5.5 Écrire le test unitaire de l'exemple chiffré Aller_Retour": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325996594
}
],
"6.3 Écrire les tests de propriété pour le calcul prévisionnel": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790325997191
}
],
"9. Frontend — Modèle, service et utilitaire d'arrondi": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790326945673
}
],
"10. Frontend — Composant `ParametresApplicationComponent` et route/menu": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327197551
}
],
"11. Frontend — Chargement initial dans `AppComponent`": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327280285
}
],
"13. Frontend — Adaptation des composants consommateurs": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327885292
}
],
"9.5 Mettre à jour `core/models/bareme.model.ts` et `core/services/bareme.service.ts` avec les nouveaux champs `MontantPrevisionnel` (`totalMoisKm`, `totalAnnuelPartielKm`, `montantPrevisionnelMensuel`, `montantPrevisionnelAnnuel`)": [
{
"executionId": "0004c7a8-345c-45b5-8865-a79b13f348d0",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790327886765
}
],
"15. Vérification finale": [
{
"executionId": "7dd406e6-29f8-4020-98c1-c18461c20a8a",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1790329415401
}
]
}
}
@@ -8,7 +8,9 @@ Ce document rassemble les **exigences techniques** dérivées des exigences fonc
Ces exigences sont destinées à être définies et arbitrées **conjointement par le PO business et le PO IT**, dans un second temps, une fois le besoin fonctionnel stabilisé. Elles peuvent évoluer sans remettre en cause le besoin métier tant que le comportement observable décrit dans `requirements.md` est respecté.
Le document est organisé **en miroir** des exigences de `requirements.md` : chaque section reprend le même numéro et le même titre suivi de « — aspects techniques », afin de faciliter les références croisées. Seules les exigences comportant effectivement des critères techniques apparaissent ici. Les exigences purement métier (Requirements 4, 13, 18) ne figurent pas dans ce document.
Le document est organisé **en miroir** des exigences de `requirements.md` : chaque section reprend le même numéro et le même titre suivi de « — aspects techniques », afin de faciliter les références croisées. Seules les exigences comportant effectivement des critères techniques apparaissent ici. Les exigences purement métier (Requirements 4, 18) ne figurent pas dans ce document.
> **Précision des distances** : les aspects techniques du paramètre Precision_Distance (endpoint, entité, formule d'arrondi) sont résumés au Requirement 22 ci-dessous, avec renvoi vers la spec séparée `precision-distances-km` (`design.md`) pour le détail complet (DTOs, service, migration).
---
@@ -23,6 +25,7 @@ Le document est organisé **en miroir** des exigences de `requirements.md` : cha
- **JWT** : JSON Web Token signé (HS256) émis par le Backend à la connexion, portant l'identifiant du Compte et une durée de validité configurable (12 heures par défaut). Support technique de la Session définie dans `requirements.md`.
- **Cache_Adresses** : La table de base de données stockant les couples (adresse textuelle normalisée BAN, coordonnées géographiques) des adresses déjà saisies, soumise à une politique de rétention LRU.
- **Mermaid** : Syntaxe de définition de diagrammes (ex. `graph TB`) rendue graphiquement lors de l'affichage des Documents_Specification.
- **ParametresApplication** : L'entité backend singleton (une seule ligne, `Id = 1`, sur le modèle de `Profil`/`Compte`) portant la colonne `PrecisionDistanceKm`, contrainte par un CHECK SQL `IN (0, 1, 2)`. Détaillée intégralement dans la spec `precision-distances-km`, `design.md`.
---
@@ -165,6 +168,16 @@ Le document est organisé **en miroir** des exigences de `requirements.md` : cha
---
### Requirement 13: Barème kilométrique et indication de versement — aspects techniques
> Exigence fonctionnelle : voir `requirements.md`, Requirement 13.
#### Acceptance Criteria
1. WHEN le Backend calcule le Montant_Previsionnel_Mensuel ou le Montant_Previsionnel_Annuel, THE Backend SHALL utiliser des distances déjà arrondies selon Precision_Distance (et non les distances brutes), conformément à la spec `precision-distances-km`, Requirements 6 et 7.
---
### Requirement 14: Montant réglé mensuel et régularisation annuelle — aspects techniques
> Exigence fonctionnelle : voir `requirements.md`, Requirement 14.
@@ -261,3 +274,17 @@ Le document est organisé **en miroir** des exigences de `requirements.md` : cha
1. THE Frontend SHALL masquer entièrement la barre de navigation (y compris l'Avatar_Utilisateur) sur les pages non authentifiées (connexion, inscription, mot de passe oublié, réinitialisation), MÊME SI un jeton résiduel expiré ou invalide est encore présent localement, la présence locale d'un jeton ne garantissant pas la validité réelle de la Session sur ces pages.
2. THE Frontend SHALL proposer, sur chaque champ de saisie d'un Mot_De_Passe (connexion, inscription, réinitialisation, changement de mot de passe), un contrôle permettant de basculer l'affichage entre masqué et visible, afin que l'utilisateur puisse vérifier la saisie ; ce contrôle SHALL présenter un rendu visuel identique à celui des autres champs de saisie du formulaire.
---
### Requirement 22: Précision configurable des distances — aspects techniques
> Exigence fonctionnelle : voir `requirements.md`, Requirement 22. Détail complet (DTOs, service, migration) dans la spec `precision-distances-km`, `design.md`.
#### Acceptance Criteria
1. THE Backend SHALL exposer les opérations de lecture et de modification de Precision_Distance via un endpoint `GET/PUT /api/parametres-application`, protégé par le même filtre d'autorisation JWT global que les autres endpoints de données, retournant 401 en l'absence de jeton valide.
2. IF le Backend reçoit sur `PUT /api/parametres-application` une valeur de Precision_Distance différente de 0, 1 ou 2, THEN THE Backend SHALL retourner une erreur HTTP 400.
3. WHEN le Backend traite avec succès une requête sur `GET/PUT /api/parametres-application`, THE Backend SHALL retourner un code HTTP 200 accompagné de la configuration courante.
4. THE Backend SHALL persister Precision_Distance dans une entité singleton `ParametresApplication` (une seule ligne, `Id = 1`), avec une contrainte CHECK SQL garantissant `PrecisionDistanceKm IN (0, 1, 2)`.
5. THE Backend SHALL calculer toute distance arrondie selon la formule `Ceiling(distanceBrute × 10^precision) / 10^precision`, appliquée à la lecture, jamais à l'écriture de la colonne `Trajet.Distance`.
+10 -3
View File
@@ -8,6 +8,8 @@
> **Documents d'exigences associés** : le besoin fonctionnel métier est décrit dans `requirements.md` ; les exigences techniques dérivées (API externes, codes HTTP, délais, sécurité, cache, rendu visuel) sont détaillées dans `design-requirements.md`. La présente conception technique met en œuvre ces exigences.
> **Précision des distances** : le paramètre de précision d'affichage des distances (Precision_Distance, 0/1/2 décimales) et son impact sur les calculs de l'application (résumés mensuels/annuels, Panneau_Totaux, barème kilométrique, carte, Export_PDF) sont documentés intégralement dans la spec séparée `precision-distances-km` (`requirements.md` et `design.md`), qui complète et prime, en cas de divergence, sur les points de conception ci-dessous relatifs au calcul des distances et du Montant_Previsionnel.
### Périmètre fonctionnel
| # | Fonctionnalité | Description |
@@ -323,8 +325,13 @@ src/
**`PanneauTotauxComponent`**
- Affiche trois totaux : total du jour ciblé par la navigation, total du mois sélectionné, Total_Annuel_Partiel (cumul de l'année jusqu'au mois sélectionné inclus)
- Reçoit en entrée la date sélectionnée et recalcule les totaux à chaque changement
- Tous les totaux affichés à 2 décimales
- Affiche également le Montant_Previsionnel minoré à 80% calculé sur le Total_Annuel_Partiel du véhicule sélectionné
- Tous les totaux affichés à 2 décimales, calculés par sommation de distances déjà arrondies selon Precision_Distance (voir spec `precision-distances-km`, Requirement 5)
- Affiche également le Montant_Previsionnel_Mensuel (80%, désormais calculé sur le seul mois affiché) et, en dessous, une nouvelle ligne « Montant prévisionnel annuel » présentant le Montant_Previsionnel_Annuel (100%, sur le Total_Annuel_Partiel) — détail complet du calcul dans la spec `precision-distances-km`, Requirements 7 et 8
**`ParametresApplicationComponent`**
- Nouveau composant du Sous_Menu_Application de la Page_Parametrage, positionné entre « Compte » et « Véhicules »
- Permet de sélectionner la valeur de Precision_Distance (0, 1 ou 2 décimales) ; la sélection courante est exposée à toute l'application via un signal partagé `precisionDistance` porté par `ParametresApplicationService`
- Détail complet (endpoints, entité, formule d'arrondi, service, migration) dans la spec `precision-distances-km`, `design.md`
**`BaremeComponent`**
- Sélecteur d'année
@@ -335,7 +342,7 @@ src/
**`MontantsReglesComponent`**
- Saisie du Montant_Regle par mois pour l'année et le véhicule sélectionnés
- Affichage en regard : Montant_Previsionnel minoré 80% (indicatif)
- Affichage en regard : Montant_Previsionnel_Mensuel minoré 80% (indicatif), désormais calculé sur le seul mois affiché (et non plus sur le cumul annuel) — voir spec `precision-distances-km`, Requirement 7
- Montants en euros à 2 décimales
**`CarnetAdressesComponent`**
@@ -63,8 +63,11 @@ L'application permet de gérer un profil (identité, matricule, adresse de domic
- **Distance_Effective** : La distance comptée d'un Trajet dans tous les agrégats (liste, totaux, carte, PDF, barème) : égale à la distance simple, ou à son double lorsque le Trajet est un Aller_Retour.
- **Document_Specification** : Un document de conception du projet consultable en lecture seule dans l'Application : les Exigences (requirements), la Conception technique (design) et le Plan de tâches (tasks).
- **Avatar_Utilisateur** : Le contrôle affiché à l'extrémité droite de la barre de navigation, représentant l'utilisateur par ses initiales (1ère lettre du Prénom + 1ère lettre du Nom). Un survol ou un clic ouvre un sous-menu proposant l'identité affichée, « Paramétrage », « Maintenance » et « Se déconnecter ».
- **Page_Parametrage** : La page accessible depuis l'Avatar_Utilisateur, présentant un menu latéral (Compte, Véhicules, Barème) et affichant à sa droite la donnée paramétrable sélectionnée.
- **Page_Parametrage** : La page accessible depuis l'Avatar_Utilisateur, présentant un menu latéral (Compte, Application, Véhicules, Barème) et affichant à sa droite la donnée paramétrable sélectionnée.
- **Page_Maintenance** : La page accessible depuis l'Avatar_Utilisateur regroupant les actions techniques de maintenance de l'Application (ex. recalcul des distances routières).
- **Precision_Distance** : Le paramètre global de l'Application, réglable dans le Sous_Menu_Application de la Page_Parametrage, déterminant le nombre de décimales (0, 1 ou 2) utilisé pour l'arrondi supérieur des distances à l'affichage et dans les agrégats (trajets, totaux, barème, carte, Export_PDF). Défini et détaillé intégralement dans la spec `precision-distances-km` (Glossary, Requirement 1).
- **Sous_Menu_Application** : L'entrée « Application » du menu latéral de la Page_Parametrage, positionnée entre « Compte » et « Véhicules », permettant de configurer Precision_Distance. Détaillée intégralement dans la spec `precision-distances-km` (Glossary, Requirement 2).
- **Montant_Previsionnel_Mensuel** et **Montant_Previsionnel_Annuel** : Les deux indications de versement affichées lors de la consultation d'un mois, redéfinies par la spec `precision-distances-km` (Glossary, Requirements 7 et 8) ; elles remplacent l'ancien Montant_Previsionnel unique décrit au Requirement 13 ci-dessous.
---
@@ -336,14 +339,17 @@ L'application permet de gérer un profil (identité, matricule, adresse de domic
**User Story:** En tant qu'utilisateur, je veux saisir le barème kilométrique officiel par année et par puissance fiscale, et voir une indication de versement mensuel, afin d'estimer mes remboursements en cours d'année.
> Le calcul du Montant_Previsionnel_Mensuel (critère 5) et du Montant_Previsionnel_Annuel (critère 5bis) est précisé intégralement par la spec `precision-distances-km`, Requirements 7 et 8, qui prime en cas de divergence.
#### Acceptance Criteria
1. THE Frontend SHALL afficher un menu de saisie du Bareme_Kilometrique par année permettant de saisir, pour chaque Puissance_Fiscale, une ou plusieurs Tranches_Bareme définies par une borne minimale de kilométrage, une borne maximale de kilométrage (la dernière tranche pouvant être illimitée, sans borne maximale), un coefficient (€/km sur trois décimales) et une base (€). L'utilisateur peut ajouter plusieurs tranches pour une même Puissance_Fiscale (ex. 0–4 999 km, 5 000–19 999 km, 20 000 km et plus).
2. WHEN l'utilisateur enregistre une Tranche_Bareme, THE Backend SHALL persister la tranche associée à l'année, à la Puissance_Fiscale et à la borne de kilométrage concernées.
3. WHEN le Backend applique le Bareme_Kilometrique au total kilométrique annuel d'un Véhicule pour une année donnée, THE Backend SHALL sélectionner la tranche dont l'intervalle de kilométrage contient le total annuel, puis calculer le montant selon la formule `montant = total_km_annuel × coefficient + base`.
4. IF aucune Tranche_Bareme n'existe pour la Puissance_Fiscale et l'année d'un calcul, THEN THE Backend SHALL retourner le montant zéro et signaler que le barème n'est pas configuré pour cette combinaison.
5. WHEN l'utilisateur consulte les données d'un mois, THE Frontend SHALL afficher à titre indicatif le Montant_Previsionnel, calculé comme 80% du Bareme_Kilometrique appliqué au Total_Annuel_Partiel du Véhicule sélectionné.
6. THE Frontend SHALL indiquer explicitement que le Montant_Previsionnel (80%) est une simple indication de versement et que le montant réellement versé est celui saisi par l'utilisateur (Montant_Regle).
5. WHEN l'utilisateur consulte les données d'un mois, THE Frontend SHALL afficher à titre indicatif le Montant_Previsionnel_Mensuel, calculé comme 80% du Bareme_Kilometrique appliqué à la Distance_Effective_Arrondie totale du seul mois affiché (et non plus du Total_Annuel_Partiel), la tranche applicable restant déterminée par le Total_Annuel_Partiel.
5bis. WHEN l'utilisateur consulte les données d'un mois, THE Frontend SHALL afficher, sous le Montant_Previsionnel_Mensuel, une ligne « Montant prévisionnel annuel » présentant le Montant_Previsionnel_Annuel, calculé comme 100% (sans minoration) du Bareme_Kilometrique appliqué au Total_Annuel_Partiel du Véhicule sélectionné.
6. THE Frontend SHALL indiquer explicitement que le Montant_Previsionnel_Mensuel (80%) est une simple indication de versement, que le Montant_Previsionnel_Annuel (100%) est une projection indicative sans minoration, et que le montant réellement versé est celui saisi par l'utilisateur (Montant_Regle).
7. THE Frontend SHALL afficher tous les montants en euros avec une précision de deux décimales.
8. THE Frontend SHALL afficher, dans l'en-tête de chaque feuille mensuelle, le Coefficient_Base_Vehicule (coefficient et base de la tranche applicable selon la puissance fiscale du Véhicule et le total kilométrique annuel).
9. THE Frontend SHALL proposer un bouton « Recopier le dernier barème connu » qui, pour l'année affichée, recopie l'intégralité des Tranches_Bareme de l'année la plus récente antérieure comportant un barème. IF des Tranches_Bareme existent déjà pour l'année affichée, THEN THE Frontend SHALL demander une confirmation avant de les remplacer intégralement (annule et remplace). THE Frontend SHALL rendre ce bouton inaccessible lorsque l'année affichée est celle du dernier barème connu (aucune année antérieure à recopier).
@@ -362,8 +368,8 @@ L'application permet de gérer un profil (identité, matricule, adresse de domic
2. WHEN l'utilisateur enregistre un Montant_Regle, THE Backend SHALL persister ce montant associé au mois, à l'année et au Véhicule concernés.
3. IF un Montant_Regle existe déjà pour le même mois, la même année et le même Véhicule, THEN THE Backend SHALL mettre à jour le montant existant plutôt que d'en créer un nouveau.
4. IF l'utilisateur saisit un Montant_Regle négatif, THEN THE Backend SHALL rejeter la saisie.
5. WHEN le Backend calcule le Montant_Reel annuel d'un Véhicule, THE Backend SHALL appliquer le Bareme_Kilometrique plein (100%) au total kilométrique annuel du Véhicule, en utilisant la tranche déterminée par ce total annuel.
6. WHEN le Backend calcule le Frais_Reel_Mensuel de chaque mois, THE Backend SHALL multiplier les kilomètres du mois par le coefficient de la tranche annuelle applicable, de sorte que la somme des douze Frais_Reel_Mensuel plus la base de la tranche annuelle soit égale au Montant_Reel annuel.
5. WHEN le Backend calcule le Montant_Reel annuel d'un Véhicule, THE Backend SHALL appliquer le Bareme_Kilometrique plein (100%) au total kilométrique annuel du Véhicule, en utilisant la tranche déterminée par ce total annuel ; ce total kilométrique annuel est constitué de distances déjà arrondies selon Precision_Distance, conformément à la spec `precision-distances-km`, Requirement 6.
6. WHEN le Backend calcule le Frais_Reel_Mensuel de chaque mois, THE Backend SHALL multiplier les kilomètres du mois par le coefficient de la tranche annuelle applicable, de sorte que la somme des douze Frais_Reel_Mensuel plus la base de la tranche annuelle soit égale au Montant_Reel annuel ; les kilomètres du mois sont également des distances déjà arrondies selon Precision_Distance (spec `precision-distances-km`, Requirement 6).
7. WHEN le Backend calcule la Regularisation d'un Véhicule pour une année, THE Backend SHALL soustraire le total des Montant_Regle de l'année du Montant_Reel annuel : `Regularisation = Montant_Reel − Total_Regle`.
8. THE Frontend SHALL afficher le Montant_Regle, le Frais_Reel_Mensuel, le Montant_Reel annuel, le total réglé et la Regularisation avec une précision de deux décimales.
@@ -500,3 +506,20 @@ L'application permet de gérer un profil (identité, matricule, adresse de domic
4. WHEN l'utilisateur sélectionne « Paramétrage », THE Frontend SHALL naviguer vers la Page_Parametrage, affichant par défaut la donnée « Compte » ; WHEN l'utilisateur sélectionne « Maintenance », THE Frontend SHALL naviguer vers la Page_Maintenance ; WHEN l'utilisateur sélectionne « Se déconnecter », THE Frontend SHALL déconnecter l'utilisateur et naviguer vers la page de connexion.
5. THE Page_Parametrage SHALL présenter un menu latéral avec les entrées « Compte » (identité, matricule, adresse de domicile), « Véhicules » (historique des véhicules) et « Barème » (barème kilométrique), chacune affichant à droite le contenu correspondant sans rechargement complet de la page.
6. THE Backend SHALL continuer à exiger la configuration du Profil (Exigence 16) pour l'accès à la donnée « Barème » de la Page_Parametrage, mais PAS pour l'accès aux données « Compte » et « Véhicules », afin de ne pas bloquer un utilisateur dont le Profil n'est pas encore configuré.
---
### Requirement 22: Précision configurable des distances et arrondi supérieur
**User Story:** En tant qu'utilisateur, je veux choisir le nombre de décimales utilisé pour l'affichage des distances en kilomètres et bénéficier d'un arrondi systématiquement supérieur, afin d'adapter la précision affichée à mes besoins tout en ayant la certitude que les distances affichées ne sont jamais sous-évaluées.
> Cette exigence est détaillée intégralement par la spec `precision-distances-km` (Requirements 1, 3, 4, 5), qui prime en cas de divergence. Les exigences techniques associées (endpoint, entité, formule d'arrondi) sont décrites dans `design-requirements.md`, Requirement 22.
#### Acceptance Criteria
1. THE Backend SHALL persister une valeur unique de Precision_Distance pour l'ensemble de l'Application, parmi exactement trois valeurs possibles : 0, 1 ou 2, avec une valeur par défaut de 2 en l'absence de valeur déjà enregistrée.
2. THE Page_Parametrage SHALL présenter, dans son menu latéral, le Sous_Menu_Application positionné entre « Compte » et « Véhicules », permettant de sélectionner la valeur de Precision_Distance parmi 0, 1 et 2.
3. WHEN le Frontend affiche la distance d'un Trajet qui n'est pas un Aller_Retour, THE Frontend SHALL afficher cette distance arrondie au supérieur à la précision définie par Precision_Distance.
4. WHEN le Frontend affiche la distance d'un Trajet dont Aller_Retour est vrai, THE Frontend SHALL afficher le double de la distance simple déjà arrondie au supérieur, l'arrondi étant toujours appliqué avant le doublement et jamais après.
5. WHEN le Backend ou le Frontend calcule un total kilométrique (jour, mois, Total_Annuel_Partiel), THE Backend ou THE Frontend SHALL additionner des distances déjà arrondies selon Precision_Distance, sans appliquer d'arrondi supplémentaire après la somme.
6. THE Backend SHALL ne jamais modifier la valeur brute stockée de la Distance d'un Trajet suite à un changement de Precision_Distance ; seuls l'affichage et les agrégats calculés à la volée sont affectés.