SchotzIAD KM Tracker : suivi des trajets kilométriques (Angular + .NET 8)
Application complète de saisie et de suivi des trajets kilométriques avec calcul de distance routière (OpenRouteService), barème kilométrique, résumés et régularisation annuelle. Backend (ASP.NET Core .NET 8, EF Core, SQLite) : - Trajets (CRUD, réordonnancement, insertion), calcul de distance ORS avec fallback Haversine, géométrie d'itinéraire stockée par trajet, recalcul en masse. - Profil, véhicules (avec dénomination), carnet d'adresses nommées, cache d'adresses. - Barème kilométrique : tranches multiples par puissance fiscale, borne max exclusive avec 'illimité' (KmMax nullable), coefficient à 3 décimales, recopie du dernier barème connu. - Résumés mensuel/annuel, montants réglés, régularisation, export PDF. Frontend (Angular 22, zoneless + signals) : - Consultation par jour, navigation temporelle synchronisée, carte Leaflet, formulaire de trajet (adresses nommées, domicile, autocomplétion BAN). - Détection de changement zoneless (indépendante de zone.js). Hébergement : Docker Compose (nginx + backend), scripts run-dev.sh / run-docker.sh.
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
# Exemple de configuration d'environnement pour Docker Compose.
|
||||||
|
# Copiez ce fichier vers `.env` puis renseignez les valeurs réelles :
|
||||||
|
# cp .env.example .env
|
||||||
|
#
|
||||||
|
# Le fichier `.env` n'est PAS commité (voir .gitignore). Ne mettez jamais de secret
|
||||||
|
# réel dans `.env.example`.
|
||||||
|
|
||||||
|
# Clé d'API OpenRouteService (ORS) utilisée par le backend pour le calcul
|
||||||
|
# d'itinéraires. Obtenez une clé sur https://openrouteservice.org/dev/#/signup
|
||||||
|
# Injectée dans le conteneur backend via ORS__ApiKey=${ORS_API_KEY}.
|
||||||
|
ORS_API_KEY=your-openrouteservice-api-key-here
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# Variables d'environnement locales (contiennent des secrets, ex. ORS_API_KEY).
|
||||||
|
# Jamais commité — utiliser .env.example comme modèle.
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Conserver le modèle documenté dans le suivi de version.
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# --- Frontend (Angular / Node) ---
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/.angular/
|
||||||
|
frontend/out-tsc/
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# --- Backend (.NET) ---
|
||||||
|
backend/**/bin/
|
||||||
|
backend/**/obj/
|
||||||
|
|
||||||
|
# --- Bases de données SQLite (données locales, non versionnées) ---
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
|
# --- Divers OS / éditeurs ---
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"specId": "6a5d45ea-e81d-4f5f-b4d0-a205872cf497", "workflowType": "requirements-first", "specType": "feature"}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,380 @@
|
|||||||
|
# Requirements Document
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
L'application **SchotzIAD KM Tracker** est une application web **mono-utilisateur** (sans authentification ni gestion de comptes) permettant de saisir et de suivre ses trajets mensuels (jour, adresse de départ, adresse d'arrivée), de calculer automatiquement les kilomètres parcourus par mois, et de consulter une vue annuelle consolidée des kilomètres parcourus.
|
||||||
|
|
||||||
|
L'application est composée d'un frontend Angular/TypeScript et d'un backend C#. La saisie des adresses s'appuie sur l'API de la **Base Adresses Nationale (BAN)** (disponible sur data.gouv.fr) pour l'autocomplétion et la résolution des coordonnées géographiques. Le calcul de distance routière utilise l'API **OpenRouteService (ORS)** uniquement lorsque les coordonnées des deux adresses sont connues. Elle cible des utilisateurs souhaitant garder un historique fiable de leurs déplacements, notamment à des fins professionnelles ou de remboursement de frais.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- **Application** : L'ensemble du système SchotzIAD KM Tracker (frontend Angular + backend C#).
|
||||||
|
- **Frontend** : L'interface utilisateur développée en Angular/TypeScript.
|
||||||
|
- **Backend** : Le serveur d'API développé en C#.
|
||||||
|
- **Utilisateur** : La personne qui utilise l'application.
|
||||||
|
- **Trajet** : Un déplacement défini par une date, une adresse de départ et une adresse d'arrivée.
|
||||||
|
- **Distance** : Le nombre de kilomètres calculés entre l'adresse de départ et l'adresse d'arrivée d'un Trajet.
|
||||||
|
- **Résumé_Mensuel** : L'agrégation des Trajets et des Distances pour un mois calendaire donné.
|
||||||
|
- **Résumé_Annuel** : L'agrégation des Résumés_Mensuels pour une année calendaire donnée.
|
||||||
|
- **Geocodeur** : Le service de la Base Adresses Nationale (BAN), accessible via l'API api.gouv.fr, utilisé pour convertir une adresse textuelle en coordonnées géographiques et proposer des suggestions d'adresses en temps réel lors de la saisie.
|
||||||
|
- **ORS** : OpenRouteService, l'API externe utilisée exclusivement pour le calcul d'itinéraire routier entre deux points géographiques dont les coordonnées sont déjà connues.
|
||||||
|
- **BAN** : Base Adresses Nationale, le référentiel officiel des adresses françaises accessible via l'API publique `https://api-adresse.data.gouv.fr`. Fournit l'autocomplétion d'adresses et les coordonnées géographiques (latitude, longitude) sans clé d'authentification.
|
||||||
|
- **Calculateur_Distance** : Le composant backend responsable du calcul de la Distance entre deux points géographiques.
|
||||||
|
- **API** : L'interface de programmation exposée par le Backend pour communiquer avec le Frontend.
|
||||||
|
- **Export_PDF** : La fonctionnalité permettant de générer un fichier PDF contenant les données d'un Résumé_Mensuel ou d'un Résumé_Annuel.
|
||||||
|
- **Carte** : La visualisation cartographique interactive affichant les itinéraires des Trajets d'une journée, rendue via Leaflet.js sur fond OpenStreetMap.
|
||||||
|
- **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, afin d'éviter des appels répétés à l'API BAN pour des adresses déjà connues.
|
||||||
|
- **Carnet_Adresses** : Le répertoire des Adresses_Nommées gérées par l'utilisateur via un menu dédié, permettant de créer, lister, modifier et supprimer des adresses réutilisables identifiées par une Dénomination, afin de simplifier la saisie des Trajets.
|
||||||
|
- **Adresse_Nommee** : Une adresse enregistrée dans le Carnet_Adresses, caractérisée par une Dénomination unique (libellé, 1 à 100 caractères) et un texte d'adresse (1 à 255 caractères) résolu via l'autocomplétion BAN, avec ses coordonnées géographiques (latitude, longitude) stockées lorsqu'elles sont disponibles.
|
||||||
|
- **Dénomination** : Le libellé libre et unique (1 à 100 caractères) identifiant une Adresse_Nommee dans le Carnet_Adresses (ex. « Bureau », « Client Dupont », « Domicile »).
|
||||||
|
- **Profil** : Les paramètres personnels de l'utilisateur, notamment son adresse de domicile.
|
||||||
|
- **Adresse_Domicile** : L'adresse personnelle de l'utilisateur enregistrée dans son Profil, utilisable en un clic lors de la saisie d'un Trajet.
|
||||||
|
- **Profil_Configure** : L'état du Profil considéré comme configuré lorsqu'un Profil est enregistré avec un nom et un prénom non vides ET qu'au moins un Véhicule (plaque, Puissance_Fiscale, Date_Effet) est enregistré. À l'inverse, le Profil est dit « non configuré » lorsqu'aucun Profil n'est enregistré avec un nom et un prénom non vides, ou lorsqu'aucun Véhicule n'est enregistré.
|
||||||
|
- **Onboarding** : Le parcours d'initialisation présenté à la première utilisation de l'Application lorsque le Profil est non configuré, invitant l'utilisateur à renseigner son identité (nom, prénom, matricule), éventuellement son Adresse_Domicile, et au moins un Véhicule avant d'accéder aux fonctionnalités de saisie et de consultation des Trajets.
|
||||||
|
- **Vue_Consultation** : La vue principale de l'Application permettant la saisie et la consultation des Trajets (Sélecteur_Temporel, zone des Trajets, Panneau_Totaux, Carte).
|
||||||
|
- **Commentaire** : Un texte optionnel associé à un Trajet, permettant à l'utilisateur d'annoter un déplacement (0 à 500 caractères).
|
||||||
|
- **Trajet_Précédent** : Le Trajet immédiatement antérieur dans la liste chronologique d'une journée, dont l'adresse d'arrivée peut servir de départ lors d'une insertion.
|
||||||
|
- **Trajet_Suivant** : Le Trajet immédiatement postérieur dans la liste chronologique d'une journée, dont l'adresse de départ peut servir d'arrivée lors d'une insertion.
|
||||||
|
- **Position** : Un entier positif associé à chaque Trajet au sein d'une même journée, déterminant son rang dans la liste affichée. L'ordre d'affichage est toujours trié par Position croissante, indépendamment de l'heure de création du Trajet.
|
||||||
|
- **Sélecteur_Temporel** : Le composant de navigation permettant de choisir successivement une année, un mois puis une journée pour consulter les Trajets correspondants.
|
||||||
|
- **Panneau_Totaux** : La zone latérale, toujours visible, affichant les totaux kilométriques du jour, du mois sélectionné et de l'année sélectionnée (cumul jusqu'au mois sélectionné inclus).
|
||||||
|
- **Total_Annuel_Partiel** : Le total kilométrique de l'année sélectionnée cumulant uniquement les Trajets jusqu'au mois sélectionné inclus, sans inclure les mois postérieurs.
|
||||||
|
- **Véhicule** : Un véhicule de l'utilisateur, caractérisé par une plaque d'immatriculation et une puissance fiscale, avec une date d'effet à partir de laquelle il devient le véhicule courant. L'historique des Véhicules est conservé.
|
||||||
|
- **Puissance_Fiscale** : La puissance administrative du Véhicule exprimée en chevaux fiscaux (CV), utilisée notamment pour le calcul du barème kilométrique.
|
||||||
|
- **Véhicule_Courant** : Le Véhicule dont la date d'effet est la plus récente parmi celles antérieures ou égales à la date du jour ; véhicule associé par défaut aux nouveaux Trajets.
|
||||||
|
- **Date_Effet** : La date (inférieure ou égale à la date du jour) à partir de laquelle un Véhicule devient le Véhicule_Courant.
|
||||||
|
- **Réaffectation** : L'opération consistant à réassocier des Trajets existants à un Véhicule différent lors d'un changement de véhicule.
|
||||||
|
- **Bareme_Kilometrique** : Le barème de remboursement kilométrique officiel (type URSSAF) pour une année donnée. Pour chaque Puissance_Fiscale, il comprend trois tranches de kilométrage annuel : jusqu'à 5 000 km, de 5 001 à 20 000 km, et au-delà de 20 000 km. Chaque tranche est définie par un coefficient (€/km) et une base (montant fixe en €, nul pour les tranches 1 et 3). La formule d'une tranche est : `montant = km × coefficient + base`.
|
||||||
|
- **Tranche_Bareme** : Une ligne du Bareme_Kilometrique pour une (année, Puissance_Fiscale, tranche de kilométrage) donnée : (borne_min_km, borne_max_km, coefficient €/km, base €).
|
||||||
|
- **Montant_Previsionnel** : Une indication de versement mensuel affichée à titre informatif, calculée comme 80% du Bareme_Kilometrique appliqué au Total_Annuel_Partiel. Ce montant est purement indicatif et n'est jamais utilisé dans le calcul de la Regularisation ; seul le Montant_Regle saisi par l'utilisateur fait foi.
|
||||||
|
- **Montant_Regle** : Le montant effectivement versé pour un mois donné, saisi librement par l'utilisateur (variable d'un mois à l'autre, forfaitaire ou non). C'est ce montant qui fait foi pour le calcul de la Regularisation.
|
||||||
|
- **Montant_Reel** : Le montant de remboursement réellement dû pour l'année, calculé en appliquant le Bareme_Kilometrique plein (100%, sans minoration) au total kilométrique annuel complet du Véhicule. La tranche applicable est déterminée par le total kilométrique annuel (et non par les kilomètres d'un mois isolé).
|
||||||
|
- **Regularisation** : La différence entre le Montant_Reel annuel et le total des Montant_Regle versés sur l'année, indiquant le montant restant à verser (ou à récupérer si négatif).
|
||||||
|
- **Frais_Reel_Mensuel** : La quote-part mensuelle du Montant_Reel, calculée en appliquant à chaque mois le coefficient de la tranche annuelle applicable (déterminée par le total annuel) : `Frais_Reel_Mensuel = km_du_mois × coefficient_tranche_annuelle`. La somme des douze Frais_Reel_Mensuel plus la base éventuelle de la tranche annuelle est égale au Montant_Reel.
|
||||||
|
- **Coefficient_Base_Vehicule** : Le coefficient et la base de la tranche du Bareme_Kilometrique correspondant à la Puissance_Fiscale du Véhicule et au total kilométrique annuel, affichés dans l'en-tête des feuilles mensuelles (ex. « Coef. 0,447 / Base 0 »).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement 1: Saisie d'un trajet
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux saisir un trajet avec une date, une adresse de départ et une adresse d'arrivée, afin de conserver un historique de mes déplacements.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Frontend SHALL afficher un formulaire de saisie contenant exactement les champs suivants : date du trajet (au format JJ/MM/AAAA), adresse de départ avec autocomplétion BAN (texte libre, 1 à 255 caractères) et adresse d'arrivée avec autocomplétion BAN (texte libre, 1 à 255 caractères).
|
||||||
|
2. WHEN l'utilisateur soumet le formulaire de saisie avec tous les champs obligatoires valides, THE Frontend SHALL envoyer les données du Trajet au Backend via l'API dans un délai maximum de 3 secondes.
|
||||||
|
3. WHEN le Backend reçoit les données d'un Trajet valide, THE Backend SHALL persister le Trajet en base de données et retourner une confirmation de création au Frontend.
|
||||||
|
4. IF le formulaire de saisie est soumis avec un ou plusieurs champs obligatoires manquants ou vides, THEN THE Frontend SHALL afficher, sans soumettre la requête au Backend, un message d'erreur identifiant chaque champ manquant ou vide.
|
||||||
|
5. IF le formulaire de saisie est soumis avec une date dont le format ne correspond pas à JJ/MM/AAAA ou dont la valeur est une date inexistante, THEN THE Frontend SHALL afficher un message d'erreur indiquant que la date est invalide sans soumettre la requête au Backend.
|
||||||
|
6. IF le Backend reçoit un Trajet dont la date est absente ou dont le format ne correspond pas à JJ/MM/AAAA, THEN THE Backend SHALL retourner une erreur HTTP 400 avec un message indiquant que la date est absente ou invalide, sans persister le Trajet.
|
||||||
|
7. IF le Backend reçoit un Trajet dont l'adresse de départ ou l'adresse d'arrivée est absente, vide ou dépasse 255 caractères, THEN THE Backend SHALL retourner une erreur HTTP 400 avec un message identifiant le champ concerné, sans persister le Trajet.
|
||||||
|
8. WHEN le Backend retourne une confirmation de création, THE Frontend SHALL afficher un message de confirmation de succès et réinitialiser les champs du formulaire de saisie.
|
||||||
|
9. IF le Frontend ne reçoit pas de réponse du Backend dans un délai de 10 secondes après la soumission, THEN THE Frontend SHALL afficher un message d'erreur indiquant que le serveur est indisponible et conserver les données saisies dans le formulaire.
|
||||||
|
10. THE Frontend SHALL afficher un champ optionnel "Commentaire" (texte libre, 0 à 500 caractères) dans le formulaire de saisie d'un Trajet.
|
||||||
|
11. WHEN le Backend persiste un Trajet, THE Backend SHALL stocker le commentaire associé au Trajet si fourni, ou null si absent.
|
||||||
|
12. IF le Backend reçoit un Trajet dont le commentaire dépasse 500 caractères, THEN THE Backend SHALL retourner une erreur HTTP 400 avec un message indiquant que le commentaire est trop long, sans persister le Trajet.
|
||||||
|
13. WHEN l'utilisateur saisit au moins 3 caractères dans le champ adresse de départ ou adresse d'arrivée, THE Frontend SHALL interroger l'API BAN (`https://api-adresse.data.gouv.fr/search/`) et afficher une liste de suggestions d'adresses françaises correspondant à la saisie dans un délai de 2 secondes.
|
||||||
|
14. WHEN l'utilisateur sélectionne une suggestion de l'API BAN, THE Frontend SHALL pré-remplir le champ adresse avec le libellé normalisé de l'adresse sélectionnée et stocker les coordonnées géographiques (latitude, longitude) associées pour transmission au Backend.
|
||||||
|
15. WHEN l'utilisateur soumet le formulaire avec une adresse sélectionnée via l'API BAN, THE Frontend SHALL transmettre au Backend les coordonnées géographiques (latitude, longitude) de chaque adresse en même temps que le texte de l'adresse.
|
||||||
|
16. IF l'API BAN est indisponible lors de la saisie, THEN THE Frontend SHALL permettre à l'utilisateur de saisir l'adresse manuellement en texte libre sans suggestion d'autocomplétion, en affichant un avertissement discret indiquant que l'autocomplétion est temporairement indisponible.
|
||||||
|
17. IF l'utilisateur soumet une adresse saisie manuellement sans passer par l'autocomplétion BAN (coordonnées non disponibles), THEN THE Frontend SHALL afficher un avertissement indiquant que la distance ne pourra pas être calculée automatiquement si les coordonnées sont absentes.
|
||||||
|
18. THE Frontend SHALL proposer, pour le champ adresse de départ et pour le champ adresse d'arrivée du formulaire de saisie, un choix entre sélectionner une Adresse_Nommee pré-enregistrée du Carnet_Adresses (par sa Dénomination) via un sélecteur et saisir une nouvelle adresse manuellement avec l'autocomplétion BAN.
|
||||||
|
19. WHEN l'utilisateur sélectionne une Adresse_Nommee (par sa Dénomination) dans le sélecteur associé au champ adresse de départ ou adresse d'arrivée, THE Frontend SHALL pré-remplir ce champ avec le texte d'adresse de l'Adresse_Nommee et associer ses coordonnées géographiques (latitude, longitude) pour transmission au Backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 2: Calcul automatique de la distance
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux que les kilomètres parcourus soient calculés automatiquement à partir des coordonnées des adresses saisies, afin de ne pas avoir à les calculer manuellement.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN le Backend reçoit un Trajet valide dont les coordonnées de départ et d'arrivée sont toutes deux connues, THE Calculateur_Distance SHALL calculer la Distance via l'API ORS en utilisant ces coordonnées.
|
||||||
|
2. WHEN OpenRouteService (ORS) retourne avec succès la distance routière entre les deux points géographiques d'un Trajet, THE Calculateur_Distance SHALL utiliser cette valeur comme Distance du Trajet, exprimée en kilomètres avec une valeur comprise entre 0,01 km et 99 999,99 km.
|
||||||
|
3. IF OpenRouteService (ORS) ne parvient pas à calculer la distance routière entre les coordonnées de départ et d'arrivée, THEN THE Calculateur_Distance SHALL calculer la Distance en utilisant la formule orthodromique (Haversine) comme fallback, sans retourner d'erreur à l'utilisateur.
|
||||||
|
4. IF le Backend reçoit un Trajet dont les coordonnées de départ ou d'arrivée sont absentes (adresse saisie manuellement sans BAN), THEN THE Backend SHALL tenter le géocodage de l'adresse manquante via l'API BAN avant d'appeler ORS ; si le géocodage BAN échoue également, THE Backend SHALL stocker le Trajet avec une Distance nulle et retourner une réponse HTTP 201 avec un avertissement indiquant que la distance n'a pas pu être calculée.
|
||||||
|
5. WHEN la Distance d'un Trajet est calculée, THE Backend SHALL stocker la valeur de la Distance en kilomètres avec une précision de deux décimales.
|
||||||
|
6. IF le Calculateur_Distance ne reçoit pas de réponse d'OpenRouteService (ORS) dans un délai de 10 secondes, THE Backend SHALL appliquer le fallback Haversine et stocker la Distance calculée orthodromiquement, sans erreur bloquante pour l'utilisateur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 3: Consultation du résumé mensuel
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux consulter le total des kilomètres parcourus pour un mois donné, afin de suivre mes déplacements mensuels.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN l'utilisateur sélectionne un mois (valeur entre 1 et 12 inclus) et une année (valeur entre 2000 et 2100 inclus), THE Frontend SHALL afficher la liste des Trajets enregistrés pour ce mois ainsi que le Résumé_Mensuel correspondant.
|
||||||
|
2. WHEN le Backend reçoit une requête de Résumé_Mensuel pour un mois (entre 1 et 12 inclus) et une année (entre 2000 et 2100 inclus) donnés, THE Backend SHALL retourner la liste des Trajets de ce mois triés par date croissante puis par Position croissante au sein de chaque journée, avec leur Distance respective et le total kilométrique du mois.
|
||||||
|
3. WHEN le Backend reçoit une requête de Résumé_Mensuel, THE Backend SHALL calculer le total kilométrique du Résumé_Mensuel en additionnant les Distances, exprimées en kilomètres à deux décimales, de tous les Trajets du mois concerné.
|
||||||
|
4. IF aucun Trajet n'est enregistré pour le mois sélectionné, THEN THE Backend SHALL retourner un Résumé_Mensuel avec une liste vide et un total kilométrique de zéro.
|
||||||
|
5. WHEN le Frontend affiche le Résumé_Mensuel, THE Frontend SHALL afficher le total kilométrique mensuel avec une précision de deux décimales.
|
||||||
|
6. IF le Backend reçoit une requête de Résumé_Mensuel avec un mois hors de la plage 1–12 ou une année hors de la plage 2000–2100, THEN THE Backend SHALL retourner une erreur HTTP 400 avec un message descriptif.
|
||||||
|
7. IF le Backend retourne une erreur lors d'une requête de Résumé_Mensuel, THEN THE Frontend SHALL afficher un message d'erreur indiquant que le chargement des données a échoué et ne pas afficher de données kilométriques partielles.
|
||||||
|
8. WHEN le Backend calcule avec succès un Résumé_Mensuel, THE Backend SHALL retourner une réponse HTTP 200 avec le Résumé_Mensuel correspondant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 4: Consultation de la vue annuelle
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux consulter le total des kilomètres parcourus pour une année entière, afin d'avoir une vision globale de mes déplacements annuels.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN l'utilisateur sélectionne une année comprise entre 2000 et 2100 inclus, THE Frontend SHALL afficher le Résumé_Annuel présentant le total kilométrique de chaque mois et le total annuel.
|
||||||
|
2. WHEN le Backend reçoit une requête de Résumé_Annuel pour une année donnée, THE Backend SHALL retourner les douze Résumés_Mensuels de cette année, chacun avec son total kilométrique.
|
||||||
|
3. WHEN le Backend reçoit une requête de Résumé_Annuel pour une année donnée, THE Backend SHALL calculer le total kilométrique annuel en additionnant les totaux kilométriques des douze Résumés_Mensuels de l'année concernée.
|
||||||
|
4. IF aucun Trajet n'est enregistré pour un mois d'une année, THEN THE Backend SHALL inclure ce mois dans le Résumé_Annuel avec un total kilométrique de zéro.
|
||||||
|
5. WHEN le Frontend affiche le Résumé_Annuel, THE Frontend SHALL afficher chaque total kilométrique mensuel et le total annuel avec une précision de deux décimales et une valeur maximale affichable de 999 999,99 km.
|
||||||
|
6. IF le Backend retourne une erreur lors d'une requête de Résumé_Annuel, THEN THE Frontend SHALL afficher un message d'erreur indiquant que le chargement des données a échoué et ne pas afficher de données kilométriques partielles.
|
||||||
|
7. THE Backend SHALL calculer le total kilométrique annuel exclusivement en additionnant les totaux kilométriques des douze Résumés_Mensuels de l'année concernée ; aucun kilométrage non rattaché à un mois ne peut être inclus dans le total annuel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 5: Modification et suppression d'un trajet
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux pouvoir modifier ou supprimer un trajet existant, afin de corriger des erreurs de saisie.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN l'utilisateur demande la modification d'un Trajet existant, THE Frontend SHALL afficher le formulaire de saisie pré-rempli avec les données actuelles du Trajet dans un délai de 500 ms.
|
||||||
|
2. WHEN l'utilisateur soumet la modification d'un Trajet avec au moins une adresse modifiée, THE Backend SHALL recalculer la Distance et mettre à jour les données du Trajet avec la nouvelle Distance.
|
||||||
|
3. WHEN l'utilisateur soumet la modification d'un Trajet sans modifier les adresses, THE Backend SHALL mettre à jour les données du Trajet sans recalculer la Distance.
|
||||||
|
4. WHEN l'utilisateur demande la suppression d'un Trajet, THE Frontend SHALL afficher une demande de confirmation avant d'envoyer la requête de suppression au Backend.
|
||||||
|
5. WHEN l'utilisateur confirme la suppression d'un Trajet, THE Backend SHALL supprimer le Trajet de la base de données.
|
||||||
|
6. IF le Backend reçoit une demande de modification ou de suppression pour un Trajet inexistant, THEN THE Backend SHALL retourner une réponse indiquant que le Trajet demandé n'existe pas et SHALL bloquer toute mise à jour du Résumé_Mensuel associée à cette opération.
|
||||||
|
7. WHEN un Trajet est modifié ou supprimé, THE Frontend SHALL mettre à jour l'affichage du Résumé_Mensuel dans un délai de 1000 ms sans nécessiter un rechargement complet de la page.
|
||||||
|
8. IF le recalcul de la Distance échoue lors d'une modification, THEN THE Backend SHALL annuler la modification et retourner une erreur descriptive, en préservant les données existantes du Trajet sans modification.
|
||||||
|
9. WHEN l'utilisateur soumet la modification d'un Trajet avec un commentaire modifié, THE Backend SHALL mettre à jour le commentaire du Trajet sans déclencher de recalcul de distance.
|
||||||
|
10. IF un Trajet possède une Distance nulle suite à l'absence de coordonnées, THEN THE Backend SHALL accepter la modification du Trajet et tenter de recalculer la Distance si les nouvelles adresses soumises disposent de coordonnées via le Cache_Adresses ou l'API BAN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 6: Persistance et cohérence des données
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux que mes données de trajets soient persistées de manière fiable, afin de les retrouver à chaque connexion.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Backend SHALL persister l'ensemble des Trajets dans une base de données relationnelle.
|
||||||
|
2. WHEN le Backend persiste un Trajet, THE Backend SHALL stocker la date du Trajet, l'adresse de départ (100 caractères maximum), l'adresse d'arrivée (100 caractères maximum) et la Distance calculée (valeur numérique positive exprimée en kilomètres, arrondie à deux décimales).
|
||||||
|
3. WHEN le Backend persiste un Trajet, IF l'un des champs obligatoires (date, adresse de départ, adresse d'arrivée, Distance) est absent ou invalide, THEN THE Backend SHALL rejeter la persistance et retourner un message d'erreur indiquant le champ manquant ou invalide sans créer d'entrée partielle.
|
||||||
|
4. WHEN un Trajet est supprimé, THE Backend SHALL mettre à jour le Résumé_Mensuel et le Résumé_Annuel correspondants en garantissant la cohérence des données en priorité ; le délai de 5 secondes est un objectif de performance et non une contrainte stricte lorsque la charge du système l'empêche.
|
||||||
|
5. THE Backend SHALL garantir que le total d'un Résumé_Mensuel est égal à la somme des Distances de tous les Trajets non supprimés du mois calendaire correspondant (année et mois identiques à la date du Trajet), avec une tolérance de 0,01 km.
|
||||||
|
6. THE Backend SHALL garantir que le total d'un Résumé_Annuel est égal à la somme des totaux des Résumés_Mensuels des douze mois de l'année calendaire correspondante, avec une tolérance de 0,01 km.
|
||||||
|
7. IF une opération d'écriture (création ou suppression de Trajet) échoue partiellement, THEN THE Backend SHALL annuler l'intégralité de l'opération et conserver l'état précédent des données sans modification partielle.
|
||||||
|
8. IF une opération d'écriture (création ou suppression de Trajet) réussit intégralement, THEN THE Backend SHALL ne pas effectuer de rollback de cette opération, quel que soit l'état du système après son exécution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 7: Export PDF des résumés
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux pouvoir exporter en PDF le résumé mensuel ou annuel de mes trajets, afin de conserver une trace imprimable pour mes notes de frais.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN l'utilisateur demande l'export PDF d'un Résumé_Mensuel, THE Backend SHALL générer un fichier PDF contenant la liste de tous les Trajets du mois (date, adresse de départ, adresse d'arrivée, distance en km) ainsi que le total kilométrique mensuel.
|
||||||
|
2. WHEN l'utilisateur demande l'export PDF d'un Résumé_Annuel, THE Backend SHALL générer un fichier PDF contenant le tableau des douze mois (mois, total kilométrique) ainsi que le total kilométrique annuel.
|
||||||
|
3. WHEN le Backend génère un Export_PDF avec succès, THE Frontend SHALL déclencher le téléchargement automatique du fichier PDF dans le navigateur de l'utilisateur. IF le mécanisme de téléchargement échoue après une génération réussie, THEN THE Frontend SHALL afficher un message d'erreur indiquant que le téléchargement a échoué.
|
||||||
|
4. THE Backend SHALL nommer le fichier PDF exporté selon le format suivant : `trajets_MM_AAAA.pdf` pour un export mensuel et `trajets_AAAA.pdf` pour un export annuel.
|
||||||
|
5. IF le Backend ne parvient pas à générer le fichier PDF dans un délai de 15 secondes, THEN THE Backend SHALL retourner une erreur et THE Frontend SHALL afficher un message indiquant que la génération du PDF a échoué.
|
||||||
|
6. WHEN le Backend génère un Export_PDF, THE Backend SHALL inclure dans le PDF : le titre "Rapport de trajets", la période couverte (mois/année ou année), et le total kilométrique de la période en en-tête du document.
|
||||||
|
7. WHEN l'utilisateur demande un export PDF (mensuel ou annuel), THE Backend SHALL n'inclure que les Trajets associés au Véhicule sélectionné et SHALL indiquer dans l'en-tête du PDF la plaque d'immatriculation et la Puissance_Fiscale du Véhicule concerné.
|
||||||
|
8. WHEN le Backend génère un Export_PDF (mensuel ou annuel), THE Backend SHALL inclure dans l'en-tête du document : le nom, le prénom, le matricule de l'utilisateur, ainsi que la plaque d'immatriculation et la Puissance_Fiscale du Véhicule concerné.
|
||||||
|
9. IF des Trajets de plusieurs Véhicules existent pour la période (mois ou année) demandée, THEN THE Backend SHALL générer un rapport PDF distinct par Véhicule.
|
||||||
|
10. WHEN le Backend génère un Export_PDF mensuel, THE Backend SHALL faire figurer dans l'en-tête : le Véhicule (plaque, puissance fiscale, coefficient et base de la tranche applicable), la période, l'adresse de départ du Profil, le total kilométrique du mois, le total kilométrique annuel cumulé, le TOTAL IK du mois (kilomètres du mois × coefficient de la tranche annuelle) et le Montant_Regle du mois.
|
||||||
|
11. WHEN le Backend génère un Export_PDF mensuel, THE Backend SHALL faire figurer la liste des Trajets du mois avec, pour chaque Trajet : la date, le motif (commentaire), l'adresse de départ, l'adresse d'arrivée et la distance en kilomètres.
|
||||||
|
12. WHEN le Backend génère un Export_PDF annuel (synthèse), THE Backend SHALL faire figurer : le nom et le prénom de l'utilisateur, l'année, l'adresse de domicile, le Bareme_Kilometrique applicable pour l'année, le Véhicule et sa puissance fiscale, la tranche applicable ; puis pour chacun des douze mois : les kilomètres parcourus, le Montant_Regle (frais payé) et le Frais_Reel_Mensuel (frais réel) ; et enfin les totaux : total kilométrique annuel, total réglé, Montant_Reel, et Regularisation (Montant_Reel − total réglé).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 8: Visualisation des trajets sur une carte
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux visualiser sur une carte les trajets effectués lors d'une journée donnée, afin d'avoir une représentation géographique de mes déplacements.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN l'utilisateur sélectionne une date, THE Frontend SHALL afficher une carte interactive centrée sur la zone géographique couvrant l'ensemble des Trajets de cette journée, ou centrée sur une position par défaut si aucun Trajet n'est disponible.
|
||||||
|
2. WHEN la carte est affichée pour une journée donnée, THE Frontend SHALL représenter chaque Trajet de la journée par un tracé d'itinéraire routier (polyline) sur la carte, avec un marqueur au point de départ et un marqueur au point d'arrivée.
|
||||||
|
3. WHEN l'utilisateur survole ou clique sur un tracé de Trajet sur la carte, THE Frontend SHALL afficher une infobulle indiquant l'adresse de départ, l'adresse d'arrivée et la distance du Trajet.
|
||||||
|
4. IF aucun Trajet n'est enregistré pour la date sélectionnée, THEN THE Frontend SHALL afficher la carte interactive vide avec un message indiquant qu'aucun trajet n'est disponible pour cette journée.
|
||||||
|
5. IF la géométrie d'itinéraire d'un Trajet n'est pas disponible (coordonnées manquantes ou ORS indisponible), THEN THE Frontend SHALL afficher ce Trajet comme une ligne droite entre le point de départ et le point d'arrivée, avec une indication visuelle que l'itinéraire est approximatif.
|
||||||
|
6. THE Frontend SHALL permettre à l'utilisateur de zoomer et de se déplacer sur la carte de manière interactive.
|
||||||
|
7. WHEN le Backend reçoit une requête de trajets pour une journée donnée avec visualisation, THE Backend SHALL retourner pour chaque Trajet les coordonnées géographiques de départ et d'arrivée ainsi que la géométrie de l'itinéraire (liste de points GeoJSON) si disponible dans le Cache_Adresses.
|
||||||
|
8. IF le service de carte (Leaflet.js ou les tuiles OpenStreetMap) est indisponible ou inaccessible, THEN THE Frontend SHALL afficher un message d'erreur indiquant que la carte ne peut pas être chargée et proposer à l'utilisateur de réessayer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 9: Cache et réutilisation des adresses géocodées
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux que les adresses que j'ai déjà saisies soient mémorisées et réutilisées automatiquement, afin d'éviter des appels répétés à l'API BAN et de gagner du temps lors de la saisie de nouveaux trajets.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN l'utilisateur sélectionne une adresse via l'autocomplétion BAN lors de la création ou modification d'un Trajet, THE Backend SHALL stocker dans le Cache_Adresses le libellé normalisé BAN de l'adresse, sa latitude et sa longitude si cette adresse n'y est pas déjà présente.
|
||||||
|
2. WHEN le Backend reçoit un Trajet dont l'adresse de départ ou d'arrivée correspond exactement à une entrée du Cache_Adresses, THE Backend SHALL utiliser les coordonnées du Cache_Adresses sans appeler l'API BAN.
|
||||||
|
3. WHEN l'utilisateur saisit au moins 3 caractères dans un champ adresse, THE Frontend SHALL d'abord interroger le Cache_Adresses local via le Backend pour afficher les suggestions d'adresses déjà connues, puis compléter avec les résultats de l'API BAN si le Cache_Adresses retourne moins de 5 suggestions.
|
||||||
|
4. WHEN l'utilisateur sélectionne une suggestion du Cache_Adresses ou de l'API BAN, THE Frontend SHALL pré-remplir le champ adresse avec le libellé complet et stocker les coordonnées associées pour transmission au Backend.
|
||||||
|
5. IF le Cache_Adresses contient plus de 1 000 entrées, THEN THE Backend SHALL conserver les 1 000 entrées les plus récemment utilisées et supprimer les plus anciennes (politique LRU).
|
||||||
|
6. THE Cache_Adresses SHALL stocker pour chaque adresse : le libellé normalisé BAN (255 caractères maximum), la latitude (précision de 6 décimales), la longitude (précision de 6 décimales) et la date de dernière utilisation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 10: Profil utilisateur, adresse de domicile et véhicules
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux gérer mon profil (identité, matricule, adresse de domicile et mes véhicules avec leur puissance fiscale), afin d'associer automatiquement chaque trajet au bon véhicule et de suivre mes kilomètres par véhicule.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Frontend SHALL afficher une page de profil permettant à l'utilisateur de saisir et de sauvegarder : son nom (1 à 100 caractères), son prénom (1 à 100 caractères), un matricule (1 à 8 caractères maximum), et une Adresse_Domicile (texte libre, 1 à 255 caractères).
|
||||||
|
2. IF le matricule saisi dépasse 8 caractères, THEN THE Frontend SHALL empêcher la saisie au-delà de 8 caractères et THE Backend SHALL rejeter tout matricule de plus de 8 caractères avec une erreur HTTP 400.
|
||||||
|
3. WHEN l'utilisateur sauvegarde une Adresse_Domicile, THE Backend SHALL géocoder l'adresse via l'API BAN pour obtenir ses coordonnées et les stocker dans le Profil ainsi que dans le Cache_Adresses ; si le géocodage BAN échoue, THE Backend SHALL persister l'Adresse_Domicile textuelle sans coordonnées.
|
||||||
|
4. THE Frontend SHALL afficher un bouton "Utiliser mon domicile" dans le formulaire de saisie d'un Trajet, à côté du champ adresse de départ et à côté du champ adresse d'arrivée.
|
||||||
|
5. WHEN l'utilisateur clique sur le bouton "Utiliser mon domicile" à côté d'un champ adresse, THE Frontend SHALL pré-remplir ce champ avec l'Adresse_Domicile enregistrée dans le Profil.
|
||||||
|
6. IF aucune Adresse_Domicile n'est enregistrée dans le Profil, THEN THE Frontend SHALL désactiver le bouton "Utiliser mon domicile" et afficher une infobulle indiquant que l'adresse de domicile n'est pas encore configurée.
|
||||||
|
7. WHEN l'utilisateur modifie l'Adresse_Domicile dans le Profil et sauvegarde, THE Backend SHALL re-géocoder la nouvelle adresse via l'API BAN pour mettre à jour les coordonnées dans le Profil et dans le Cache_Adresses.
|
||||||
|
8. IF le géocodage BAN de l'Adresse_Domicile échoue, THEN THE Backend SHALL sauvegarder l'Adresse_Domicile textuelle sans coordonnées et THE Frontend SHALL afficher un avertissement indiquant que l'adresse n'a pas pu être vérifiée.
|
||||||
|
9. THE Frontend SHALL permettre à l'utilisateur de saisir un Véhicule caractérisé par une plaque d'immatriculation (1 à 15 caractères) et une Puissance_Fiscale (entier positif, exprimé en chevaux fiscaux).
|
||||||
|
10. WHEN l'utilisateur enregistre un nouveau Véhicule, THE Frontend SHALL exiger la saisie d'une Date_Effet inférieure ou égale à la date du jour.
|
||||||
|
11. IF l'utilisateur saisit une Date_Effet postérieure à la date du jour, THEN THE Frontend SHALL rejeter la saisie et afficher un message indiquant que la date d'effet ne peut pas être dans le futur.
|
||||||
|
12. WHEN le Backend enregistre un nouveau Véhicule, THE Backend SHALL conserver l'historique complet des Véhicules précédents sans les supprimer.
|
||||||
|
13. THE Backend SHALL déterminer le Véhicule_Courant comme étant le Véhicule dont la Date_Effet est la plus récente parmi celles inférieures ou égales à la date du jour.
|
||||||
|
14. WHEN l'utilisateur crée un nouveau Trajet, THE Backend SHALL associer par défaut ce Trajet à la Puissance_Fiscale du Véhicule_Courant à la date du Trajet et stocker cette Puissance_Fiscale sur le Trajet.
|
||||||
|
15. WHEN l'utilisateur enregistre un nouveau Véhicule avec une Date_Effet donnée, IF il existe des Trajets déjà saisis dont la date est supérieure ou égale à cette Date_Effet et qui sont associés à une Puissance_Fiscale différente de celle du nouveau Véhicule, THEN THE Frontend SHALL proposer à l'utilisateur la Réaffectation de ces Trajets à la Puissance_Fiscale du nouveau Véhicule.
|
||||||
|
16. WHEN l'utilisateur accepte la Réaffectation, THE Backend SHALL mettre à jour la Puissance_Fiscale (et l'association au Véhicule) de tous les Trajets concernés (date >= Date_Effet) avec celle du nouveau Véhicule.
|
||||||
|
17. WHEN l'utilisateur refuse la Réaffectation, THE Backend SHALL laisser inchangés les Trajets déjà saisis et associer uniquement les prochains Trajets au nouveau Véhicule.
|
||||||
|
18. THE Backend SHALL exposer la liste de l'historique des Véhicules avec leur plaque, leur Puissance_Fiscale et leur Date_Effet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 11: Insertion d'un trajet entre deux saisies existantes
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux insérer un nouveau trajet entre deux trajets existants d'une même journée, afin de compléter une journée de déplacements en conservant la continuité des adresses.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN l'utilisateur demande l'insertion d'un Trajet entre deux Trajets existants d'une même journée (Trajet_Précédent et Trajet_Suivant), THE Frontend SHALL afficher le formulaire de saisie avec le champ adresse de départ pré-rempli avec l'adresse d'arrivée du Trajet_Précédent et le champ adresse d'arrivée pré-rempli avec l'adresse de départ du Trajet_Suivant.
|
||||||
|
2. WHEN l'utilisateur choisit d'insérer un Trajet après un Trajet_Précédent sans Trajet_Suivant connu, THE Frontend SHALL afficher le formulaire de saisie avec le champ adresse de départ pré-rempli avec l'adresse d'arrivée du Trajet_Précédent et le champ adresse d'arrivée vide.
|
||||||
|
3. WHEN l'utilisateur choisit d'insérer un Trajet avant un Trajet_Suivant sans Trajet_Précédent connu, THE Frontend SHALL afficher le formulaire de saisie avec le champ adresse d'arrivée pré-rempli avec l'adresse de départ du Trajet_Suivant et le champ adresse de départ vide.
|
||||||
|
4. THE Frontend SHALL permettre à l'utilisateur de modifier les adresses de départ et d'arrivée pré-remplies avant de soumettre le formulaire d'insertion.
|
||||||
|
5. WHEN l'utilisateur soumet le formulaire d'insertion avec tous les champs obligatoires valides, THE Backend SHALL créer le nouveau Trajet avec les mêmes règles de validation et de calcul de distance que la création standard (Exigence 1 et 2).
|
||||||
|
6. WHEN un Trajet est inséré avec succès, THE Frontend SHALL rafraîchir l'affichage de la liste des Trajets de la journée sans rechargement complet de la page, en positionnant le nouveau Trajet à sa place chronologique dans la liste.
|
||||||
|
7. THE Backend SHALL maintenir pour chaque Trajet un champ Position (entier positif) représentant son rang dans la liste des Trajets d'une même journée, indépendamment de l'heure de création du Trajet.
|
||||||
|
8. WHEN l'utilisateur insère un Trajet entre un Trajet_Précédent de position P et un Trajet_Suivant de position P+1 ou plus, THE Backend SHALL attribuer au Trajet inséré une Position comprise entre P et P+1 (valeur fractionnaire ou réordonnancement) de sorte que la liste des Trajets de la journée soit triée par Position croissante et que le Trajet inséré apparaisse bien entre les deux trajets existants.
|
||||||
|
9. WHEN l'utilisateur réordonne manuellement les Trajets d'une journée (glisser-déposer), THE Backend SHALL mettre à jour les valeurs de Position des Trajets concernés de manière à refléter le nouvel ordre sans modifier les autres données du Trajet.
|
||||||
|
10. THE Frontend SHALL proposer, dans le formulaire d'insertion, pour le champ adresse de départ et pour le champ adresse d'arrivée (y compris lorsqu'ils sont pré-remplis à partir du Trajet_Précédent ou du Trajet_Suivant), un choix entre sélectionner une Adresse_Nommee pré-enregistrée du Carnet_Adresses (par sa Dénomination) et saisir une nouvelle adresse manuellement avec l'autocomplétion BAN.
|
||||||
|
11. WHEN l'utilisateur sélectionne une Adresse_Nommee (par sa Dénomination) dans le formulaire d'insertion pour le champ adresse de départ ou adresse d'arrivée, THE Frontend SHALL remplacer le contenu pré-rempli de ce champ par le texte d'adresse de l'Adresse_Nommee et associer ses coordonnées géographiques (latitude, longitude) pour transmission au Backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 12: Navigation temporelle et mise en page
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux naviguer facilement entre les années, mois et journées ayant des données, et voir en permanence les totaux et la carte, afin de consulter et saisir mes trajets de façon fluide.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Frontend SHALL afficher un Sélecteur_Temporel permettant de choisir successivement une année, puis un mois, puis une journée.
|
||||||
|
2. THE Frontend SHALL proposer dans le sélecteur d'année uniquement l'année courante et les années pour lesquelles au moins un Trajet est enregistré.
|
||||||
|
3. WHEN l'utilisateur sélectionne une année, THE Frontend SHALL afficher les douze mois de cette année et mettre en surbrillance les mois comportant au moins un Trajet.
|
||||||
|
4. WHEN l'utilisateur sélectionne un mois, THE Frontend SHALL afficher les jours de ce mois et mettre en surbrillance les jours comportant au moins un Trajet.
|
||||||
|
5. THE Frontend SHALL empêcher la sélection de toute date postérieure à la date courante (aucune date future sélectionnable).
|
||||||
|
6. THE Frontend SHALL afficher la zone des Trajets de la journée sélectionnée comme zone principale, avec défilement vertical (scroll) lorsque le nombre de Trajets dépasse la hauteur visible.
|
||||||
|
7. THE Frontend SHALL afficher un Panneau_Totaux latéral toujours visible contenant : le total kilométrique du jour sélectionné, le total kilométrique du mois sélectionné, et le Total_Annuel_Partiel de l'année sélectionnée.
|
||||||
|
8. THE Panneau_Totaux SHALL calculer le Total_Annuel_Partiel en cumulant uniquement les Trajets de l'année sélectionnée jusqu'au mois sélectionné inclus, sans inclure les mois postérieurs au mois sélectionné.
|
||||||
|
9. THE Frontend SHALL afficher, sous le Panneau_Totaux, la Carte des Trajets de la journée sélectionnée.
|
||||||
|
10. THE Frontend SHALL afficher un bouton "Jour précédent" et un bouton "Jour suivant" permettant de naviguer d'une journée à l'autre.
|
||||||
|
11. WHEN l'utilisateur clique sur "Jour précédent", THE Frontend SHALL sélectionner la journée calendaire immédiatement antérieure et rafraîchir la zone des Trajets, le Panneau_Totaux et la Carte.
|
||||||
|
12. WHEN l'utilisateur clique sur "Jour suivant", THE Frontend SHALL sélectionner la journée calendaire immédiatement postérieure et rafraîchir la zone des Trajets, le Panneau_Totaux et la Carte.
|
||||||
|
13. IF la journée suivante est postérieure à la date courante, THEN THE Frontend SHALL désactiver le bouton "Jour suivant".
|
||||||
|
14. WHEN l'utilisateur change de jour, de mois ou d'année via le Sélecteur_Temporel ou les boutons de navigation, THE Frontend SHALL mettre à jour la zone des Trajets, le Panneau_Totaux et la Carte dans un délai de 1000 ms sans rechargement complet de la page.
|
||||||
|
15. WHEN le Backend reçoit une requête des années disponibles, THE Backend SHALL retourner la liste des années comportant au moins un Trajet, ainsi que l'année courante si elle n'y figure pas déjà.
|
||||||
|
16. WHEN le Backend reçoit une requête des jours comportant des Trajets pour un mois et une année donnés, THE Backend SHALL retourner la liste des jours de ce mois pour lesquels au moins un Trajet est enregistré.
|
||||||
|
17. THE Frontend SHALL afficher un sélecteur de Véhicule permettant de choisir le Véhicule dont les données sont affichées ; par défaut, le Véhicule_Courant est sélectionné.
|
||||||
|
18. WHEN l'utilisateur sélectionne un Véhicule dans le sélecteur de Véhicule, THE Frontend SHALL filtrer la zone des Trajets, le Panneau_Totaux et la Carte pour n'afficher que les Trajets associés à ce Véhicule.
|
||||||
|
19. THE Panneau_Totaux SHALL calculer le total du jour, le total du mois et le Total_Annuel_Partiel uniquement à partir des Trajets associés au Véhicule sélectionné.
|
||||||
|
20. WHEN l'utilisateur change de Véhicule sélectionné, THE Frontend SHALL mettre à jour la zone des Trajets, le Panneau_Totaux et la Carte dans un délai de 1000 ms sans rechargement complet de la page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 13: Barème kilométrique et indication de versement
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Frontend SHALL afficher un menu de saisie du Bareme_Kilometrique par année, permettant de saisir, pour chaque Puissance_Fiscale, les trois tranches de kilométrage : jusqu'à 5 000 km, de 5 001 à 20 000 km, et au-delà de 20 000 km, chacune avec son coefficient (€/km) et sa base (€).
|
||||||
|
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).
|
||||||
|
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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 14: Montant réglé mensuel et régularisation annuelle
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux saisir le montant réellement versé chaque mois et voir la régularisation annuelle, afin de connaître le solde restant à verser en fin d'année.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Frontend SHALL permettre à l'utilisateur de saisir un Montant_Regle (valeur numérique positive en euros, deux décimales) pour un mois et une année donnés, associé à un Véhicule ; ce montant est saisi librement et peut différer d'un mois à l'autre.
|
||||||
|
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 avec une erreur HTTP 400.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 15: Carnet d'adresses nommées
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux gérer un carnet d'adresses nommées (créer, lister, modifier, supprimer) et pouvoir réutiliser ces adresses par leur dénomination lors de la saisie d'un trajet, afin de simplifier et d'accélérer la saisie de mes déplacements.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Frontend SHALL afficher un menu dédié au Carnet_Adresses permettant à l'utilisateur de créer, lister, modifier et supprimer des Adresses_Nommées.
|
||||||
|
2. THE Frontend SHALL afficher, pour la création ou la modification d'une Adresse_Nommee, un champ Dénomination (texte libre, 1 à 100 caractères) et un champ adresse avec autocomplétion BAN (texte libre, 1 à 255 caractères).
|
||||||
|
3. WHEN l'utilisateur saisit au moins 3 caractères dans le champ adresse d'une Adresse_Nommee, THE Frontend SHALL interroger l'API BAN après un délai d'inactivité de 300 millisecondes et afficher au maximum 5 suggestions.
|
||||||
|
4. WHEN l'utilisateur sélectionne une suggestion de l'API BAN lors de la saisie du champ adresse d'une Adresse_Nommee, THE Frontend SHALL pré-remplir le champ avec le libellé normalisé et associer les coordonnées géographiques (latitude, longitude) pour transmission au Backend.
|
||||||
|
5. WHEN le Backend reçoit une Adresse_Nommee valide, THE Backend SHALL persister en base de données la Dénomination, le texte d'adresse et, lorsqu'elles sont disponibles, les coordonnées géographiques (latitude, longitude).
|
||||||
|
6. IF le Backend reçoit une Adresse_Nommee dont la Dénomination est absente, vide ou dépasse 100 caractères, THEN THE Backend SHALL retourner une erreur HTTP 400 avec un message indiquant que la Dénomination est absente ou invalide, sans persister l'Adresse_Nommee.
|
||||||
|
7. IF le Backend reçoit une Adresse_Nommee dont la Dénomination est identique, après normalisation (suppression des espaces de début et de fin, comparaison insensible à la casse et aux accents), à celle d'une Adresse_Nommee déjà enregistrée, THEN THE Backend SHALL retourner une erreur HTTP 400 avec un message indiquant que la Dénomination est déjà utilisée, sans persister l'Adresse_Nommee.
|
||||||
|
8. IF le Backend reçoit une Adresse_Nommee dont le texte d'adresse est absent, vide ou dépasse 255 caractères, THEN THE Backend SHALL retourner une erreur HTTP 400 avec un message indiquant que l'adresse est absente ou invalide, sans persister l'Adresse_Nommee.
|
||||||
|
9. WHEN le Backend persiste une Adresse_Nommee dont les coordonnées géographiques sont disponibles, THE Backend SHALL enregistrer le couple (libellé normalisé, latitude, longitude) dans le Cache_Adresses s'il n'y est pas déjà présent, afin que cette adresse soit réutilisable et mise en cache.
|
||||||
|
10. WHEN le Backend reçoit une Adresse_Nommee dont le texte d'adresse correspond, après normalisation (suppression des espaces de début et de fin, comparaison insensible à la casse et aux accents), à une entrée du Cache_Adresses, THE Backend SHALL réutiliser les coordonnées du Cache_Adresses sans appeler l'API BAN.
|
||||||
|
11. WHEN l'utilisateur modifie le texte d'adresse d'une Adresse_Nommee existante et enregistre, et que ce texte ne correspond à aucune entrée du Cache_Adresses après normalisation, THE Backend SHALL re-géocoder la nouvelle adresse via l'API BAN dans un délai maximal de 5 secondes afin de rafraîchir les coordonnées géographiques (latitude, longitude) de l'Adresse_Nommee.
|
||||||
|
12. IF l'API BAN est indisponible ou ne répond pas dans le délai de 5 secondes lors du géocodage ou du re-géocodage d'une Adresse_Nommee, THEN THE Backend SHALL persister l'Adresse_Nommee sans coordonnées géographiques et retourner une réponse indiquant que les coordonnées n'ont pas pu être obtenues.
|
||||||
|
13. WHEN l'utilisateur demande la suppression d'une Adresse_Nommee, THE Frontend SHALL afficher une demande de confirmation avant d'envoyer la requête de suppression au Backend.
|
||||||
|
14. WHEN l'utilisateur confirme la suppression d'une Adresse_Nommee, THE Backend SHALL supprimer l'Adresse_Nommee du Carnet_Adresses en conservant inchangés les Trajets déjà enregistrés, qui gardent leur texte d'adresse et leurs coordonnées géographiques stockés.
|
||||||
|
15. WHEN le Backend a créé, modifié ou supprimé une Adresse_Nommee avec succès, THE Frontend SHALL afficher un message de confirmation indiquant que l'opération a réussi.
|
||||||
|
16. WHEN le Frontend affiche le formulaire de saisie ou d'insertion d'un Trajet, THE Frontend SHALL proposer la liste des Adresses_Nommées du Carnet_Adresses par leur Dénomination pour le champ adresse de départ et pour le champ adresse d'arrivée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement 16: Initialisation du profil à la première utilisation
|
||||||
|
|
||||||
|
**User Story:** En tant qu'utilisateur, je veux que l'application me présente la saisie de mon profil dès la première connexion, afin de configurer mon identité et mon véhicule avant de commencer à saisir mes trajets.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Backend SHALL considérer le Profil comme non configuré tant qu'aucun Profil n'est enregistré avec un nom et un prénom non vides, ou tant qu'aucun Véhicule (plaque, Puissance_Fiscale, Date_Effet) n'est enregistré.
|
||||||
|
2. THE Backend SHALL exposer un indicateur de configuration du Profil (`estConfigure`) valant vrai lorsque le Profil est un Profil_Configure et faux dans le cas contraire.
|
||||||
|
3. WHEN l'utilisateur ouvre l'Application, WHILE le Profil est non configuré, THE Frontend SHALL présenter le parcours d'Onboarding (formulaire de saisie du Profil) au lieu de la Vue_Consultation.
|
||||||
|
4. WHILE le Profil est non configuré, THE Frontend SHALL bloquer l'accès à la saisie et à la consultation des Trajets et maintenir l'affichage du parcours d'Onboarding.
|
||||||
|
5. THE Frontend SHALL exiger, dans le parcours d'Onboarding, la saisie du nom (1 à 100 caractères), du prénom (1 à 100 caractères) et du matricule (1 à 8 caractères) avant d'autoriser la validation du Profil.
|
||||||
|
6. THE Frontend SHALL exiger, dans le parcours d'Onboarding, la saisie d'au moins un Véhicule comportant une plaque d'immatriculation (1 à 15 caractères), une Puissance_Fiscale (entier positif) et une Date_Effet inférieure ou égale à la date du jour avant d'autoriser la validation du Profil.
|
||||||
|
7. THE Frontend SHALL présenter, dans le parcours d'Onboarding, un champ Adresse_Domicile optionnel (texte libre, 1 à 255 caractères) que l'utilisateur peut renseigner immédiatement ou compléter ultérieurement via la page de profil.
|
||||||
|
8. IF l'utilisateur tente de valider le parcours d'Onboarding alors qu'un champ d'identité obligatoire (nom, prénom, matricule) est manquant ou invalide, ou qu'aucun Véhicule valide n'est saisi, THEN THE Frontend SHALL afficher un message d'erreur identifiant chaque élément manquant ou invalide sans soumettre les données au Backend.
|
||||||
|
9. WHEN l'utilisateur valide le parcours d'Onboarding avec un Profil et au moins un Véhicule valides, THE Backend SHALL persister le Profil et le Véhicule et retourner un indicateur `estConfigure` valant vrai.
|
||||||
|
10. WHEN le Backend confirme l'enregistrement d'un Profil_Configure, THE Frontend SHALL marquer le Profil comme configuré et naviguer vers la Vue_Consultation dans un délai de 1000 ms.
|
||||||
|
11. IF le Profil est déjà un Profil_Configure lors de l'ouverture de l'Application, THEN THE Frontend SHALL afficher directement la Vue_Consultation sans présenter le parcours d'Onboarding.
|
||||||
|
12. IF la persistance du Profil ou du Véhicule échoue au niveau du Backend lors de la soumission du parcours d'Onboarding, THEN THE Frontend SHALL afficher un message d'erreur indiquant l'échec de l'enregistrement, ne pas marquer le Profil comme configuré et conserver les données saisies par l'utilisateur.
|
||||||
|
13. IF la récupération de l'indicateur `estConfigure` échoue lors de l'ouverture de l'Application, THEN THE Frontend SHALL afficher un message d'erreur indiquant un problème de connexion au serveur et proposer de réessayer, sans présenter la Vue_Consultation.
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
# Implementation Plan: SchotzIAD KM Tracker
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Ce plan décompose la conception en tâches de codage incrémentales. Le backend (ASP.NET Core / C# / EF Core / SQLite) est construit d'abord — entités, calculs métier, intégrations BAN/ORS, PDF — puis le frontend Angular consomme l'API. Chaque tâche s'appuie sur les précédentes et se termine par l'intégration, sans code orphelin. Les sous-tâches de test (unitaire, propriété, intégration) sont marquées optionnelles par `*` et référencent explicitement les propriétés de correction et exigences validées.
|
||||||
|
|
||||||
|
Langage backend : **C# (ASP.NET Core, .NET 8)**. Langage frontend : **TypeScript (Angular)**. Tests : **xUnit + FsCheck + FluentAssertions + Moq** (backend), **Jest + Angular Testing Library** (frontend).
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- [x] 1. Initialiser la solution et le socle backend
|
||||||
|
- Créer la solution .NET et le projet `TrajetKmTracker.Api` (ASP.NET Core Web API, .NET 8)
|
||||||
|
- Créer le projet de tests `TrajetKmTracker.Tests` (xUnit) avec les dépendances FsCheck, FluentAssertions, Moq, Microsoft.AspNetCore.Mvc.Testing
|
||||||
|
- Ajouter les packages : EF Core + provider SQLite, FluentValidation.AspNetCore, QuestPDF
|
||||||
|
- Configurer `Program.cs` : services de base, Swagger, middleware `ProblemDetails`, CORS pour le frontend Angular
|
||||||
|
- _Requirements: 6.1_
|
||||||
|
|
||||||
|
- [x] 2. Modèle de données et contexte EF Core
|
||||||
|
- [x] 2.1 Définir les entités du domaine
|
||||||
|
- Créer `Trajet` (Date, AdresseDepart, AdresseArrivee, Distance, Commentaire, Position REAL, VehiculeId, PuissanceFiscale dénormalisée, CreatedAt/UpdatedAt)
|
||||||
|
- Créer `Vehicule` (Plaque, PuissanceFiscale, DateEffet), `Profil` (singleton Id=1, Nom/Prenom/Matricule/AdresseDomicile + coordonnées + DomicileGeocode)
|
||||||
|
- Créer `AdresseCache` (Adresse unique, Latitude/Longitude, GeometrieJson, DerniereUtilisation), `TrancheBareme` (Annee, PuissanceFiscale, KmMin, KmMax, Coefficient, Base), `MontantRegle` (Annee, Mois, VehiculeId, Montant)
|
||||||
|
- Créer `AdresseNommee` (Denomination unique 100 car., DenominationNormalisee, Adresse 255 car., Latitude?, Longitude?, GeocodageReussi, CreatedAt/UpdatedAt)
|
||||||
|
- _Requirements: 6.1, 6.2, 9.6, 10.1, 10.9, 13.1, 14.1, 15.4, 15.5_
|
||||||
|
|
||||||
|
- [x] 2.2 Configurer `AppDbContext` et les migrations
|
||||||
|
- Déclarer les `DbSet`, contraintes (CHECK Distance > 0, Montant >= 0, PuissanceFiscale > 0), index (Date, Date+Position, VehiculeId, DateEffet, Annee+PuissanceFiscale) et index uniques (Profil singleton, MontantsRegles par période/véhicule, AdresseCache par adresse)
|
||||||
|
- Ajouter la table `AdressesNommees` avec l'index unique `UX_AdresseNommee_Denomination` sur `DenominationNormalisee`
|
||||||
|
- Générer la migration initiale et l'appliquer à `trajets.db`
|
||||||
|
- _Requirements: 6.1, 6.2, 9.6, 14.3, 15.6_
|
||||||
|
|
||||||
|
- [ ]* 2.3 Écrire les tests d'intégration du schéma
|
||||||
|
- Vérifier la création de la base, les contraintes CHECK et les index uniques (rejet des doublons de période/véhicule)
|
||||||
|
- _Requirements: 6.1, 14.3_
|
||||||
|
|
||||||
|
- [x] 3. DTOs, options de configuration et validateurs
|
||||||
|
- [x] 3.1 Créer les DTOs et enregistrements
|
||||||
|
- `CreateTrajetDto`, `UpdateTrajetDto`, `TrajetDto`, `TrajetCarteDto`, `ReordonnerTrajetsDto`, `TrajetPositionDto`
|
||||||
|
- `ResumeMensuelDto`, `ResumeAnnuelDto`, `TotauxDto`, `AdresseCacheDto`, `BANAdresseDto`
|
||||||
|
- `ProfilDto`, `SaveProfilDto`, `VehiculeDto`, `CreateVehiculeDto`, `VehiculeCreeDto`, `ReaffectationDto`
|
||||||
|
- `TrancheBaremeDto`, `SaveTrancheBaremeDto`, `MontantRegleDto`, `MontantPrevisionnelDto`, `LigneSyntheseMoisDto`, `RegularisationAnnuelleDto`
|
||||||
|
- `AdresseNommeeDto`, `SaveAdresseNommeeDto`
|
||||||
|
- `EtatProfilDto` (EstConfigure)
|
||||||
|
- Créer `BANOptions` et `ORSOptions`, les lier depuis `appsettings.json`
|
||||||
|
- _Requirements: 1.1, 1.10, 2.1, 15.4, 15.5_
|
||||||
|
|
||||||
|
- [x] 3.2 Implémenter les validateurs FluentValidation
|
||||||
|
- `CreateTrajetDtoValidator` / `UpdateTrajetDtoValidator` : date obligatoire au format JJ/MM/AAAA et existante, adresses 1–255 car., commentaire ≤ 500 car.
|
||||||
|
- `SaveProfilDtoValidator` (matricule ≤ 8 car.), `CreateVehiculeDtoValidator` (DateEffet ≤ aujourd'hui), `MontantRegleDto` (montant ≥ 0)
|
||||||
|
- `SaveAdresseNommeeDtoValidator` : dénomination 1–100 car. (absente/vide/>100 → 15.6), adresse 1–255 car. (absente/vide/>255 → 15.8) ; l'unicité normalisée (doublon → 15.7) est vérifiée côté service
|
||||||
|
- _Requirements: 1.4, 1.5, 1.6, 1.7, 1.12, 6.3, 10.2, 10.11, 14.4, 15.6, 15.7, 15.8_
|
||||||
|
|
||||||
|
- [ ]* 3.3 Écrire les tests unitaires des validateurs
|
||||||
|
- **Property 4 : Rejet systématique des données d'entrée invalides (backend)**
|
||||||
|
- **Validates: Requirements 1.5, 1.6, 1.7, 6.3**
|
||||||
|
|
||||||
|
- [x] 4. Intégrations externes : BAN et ORS
|
||||||
|
- [x] 4.1 Implémenter `IBANClient` / `BANClient`
|
||||||
|
- Appel `GET /search/?q={query}&limit=5` avec timeout configurable ; mapper vers `BANAdresseDto` ; lever `BANIndisponibleException` en cas d'échec/timeout
|
||||||
|
- _Requirements: 1.13, 2.4, 9.1_
|
||||||
|
|
||||||
|
- [x] 4.2 Implémenter `IORSClient` / `ORSClient`
|
||||||
|
- Appel `POST /v2/directions/driving-car` avec coordonnées connues, extraction distance (km) + géométrie GeoJSON, timeout 10 s ; ne jamais appeler ORS avec des coordonnées nulles
|
||||||
|
- _Requirements: 2.1, 2.2, 8.7_
|
||||||
|
|
||||||
|
- [x] 4.3 Implémenter `IDistanceCalculator` / `DistanceCalculatorService`
|
||||||
|
- Coordonnées connues → ORS ; coordonnées manquantes → géocodage BAN puis ORS ; échec ORS/timeout → fallback Haversine ; échec BAN total → distance nulle + avertissement
|
||||||
|
- Implémenter `HaversineKm`, arrondi 2 décimales, borne 0,01–99 999,99 km
|
||||||
|
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.6_
|
||||||
|
|
||||||
|
- [ ]* 4.4 Écrire le test de propriété de priorité BAN sur ORS
|
||||||
|
- **Property 16 : Priorité BAN sur ORS pour la résolution des coordonnées**
|
||||||
|
- **Validates: Requirements 2.4**
|
||||||
|
|
||||||
|
- [ ]* 4.5 Écrire les tests unitaires BAN/ORS/Distance
|
||||||
|
- BAN succès/non trouvé/timeout, ORS succès/timeout → Haversine, coordonnées manquantes → BAN puis ORS
|
||||||
|
- _Requirements: 2.1, 2.2, 2.3, 2.6_
|
||||||
|
|
||||||
|
- [x] 5. Cache des adresses géocodées
|
||||||
|
- [x] 5.1 Implémenter `AdresseCacheService` et le repository associé
|
||||||
|
- Recherche locale (min 3 car.), upsert d'une adresse (coordonnées inchangées si existante, mise à jour de `DerniereUtilisation`), éviction LRU au-delà de 1 000 entrées
|
||||||
|
- _Requirements: 9.1, 9.2, 9.3, 9.5, 9.6_
|
||||||
|
|
||||||
|
- [ ]* 5.2 Écrire le test de propriété de cohérence du cache
|
||||||
|
- **Property 10 : Cohérence du cache adresses après géocodage**
|
||||||
|
- **Validates: Requirements 9.1, 9.2**
|
||||||
|
|
||||||
|
- [ ]* 5.3 Écrire le test de propriété LRU
|
||||||
|
- **Property 11 : Politique LRU du cache adresses**
|
||||||
|
- **Validates: Requirements 9.5**
|
||||||
|
|
||||||
|
- [x] 6. Gestion des véhicules et du profil
|
||||||
|
- [x] 6.1 Implémenter `VehiculeService` et le repository véhicules
|
||||||
|
- CRUD historique, détermination du Véhicule_Courant (DateEffet max ≤ aujourd'hui), rejet DateEffet future, conservation de l'historique
|
||||||
|
- Détection des trajets à réaffecter (date ≥ DateEffet et PuissanceFiscale différente) → `VehiculeCreeDto.ReaffectationProposee`
|
||||||
|
- Réaffectation acceptée (mise à jour VehiculeId + PuissanceFiscale des trajets ≥ DateEffet) / refusée (aucun changement), en transaction
|
||||||
|
- _Requirements: 10.9, 10.10, 10.11, 10.12, 10.13, 10.15, 10.16, 10.17, 10.18_
|
||||||
|
|
||||||
|
- [ ]* 6.2 Écrire le test de propriété d'association du véhicule courant
|
||||||
|
- **Property 19 : Association du véhicule courant par défaut**
|
||||||
|
- **Validates: Requirements 10.13, 10.14**
|
||||||
|
|
||||||
|
- [ ]* 6.3 Écrire le test de propriété de réaffectation bornée
|
||||||
|
- **Property 20 : Réaffectation bornée à la date d'effet**
|
||||||
|
- **Validates: Requirements 10.16, 10.17**
|
||||||
|
|
||||||
|
- [ ]* 6.4 Écrire le test de propriété de conservation de l'historique
|
||||||
|
- **Property 22 : Conservation de l'historique des véhicules**
|
||||||
|
- **Validates: Requirements 10.12, 10.18**
|
||||||
|
|
||||||
|
- [x] 6.5 Implémenter `ProfilService` et le controller `ProfilController`
|
||||||
|
- GET/PUT profil singleton, géocodage BAN de l'Adresse_Domicile (DomicileGeocode true/false), mise en cache, validation matricule ≤ 8 car.
|
||||||
|
- Exposer `GET /api/vehicules`, `GET /api/vehicules/courant`, `POST /api/vehicules`, `POST /api/vehicules/reaffectation`
|
||||||
|
- Exposer `GET /api/profil/etat` retournant `EtatProfilDto(EstConfigure)` (true ssi Profil Nom+Prénom non vides ET au moins un Véhicule)
|
||||||
|
- _Requirements: 10.1, 10.2, 10.3, 10.7, 10.8, 10.18, 16.1, 16.2_
|
||||||
|
|
||||||
|
- [ ]* 6.6 Écrire le test de propriété de cohérence du profil domicile
|
||||||
|
- **Property 12 : Cohérence du profil domicile**
|
||||||
|
- **Validates: Requirements 10.2, 10.7**
|
||||||
|
|
||||||
|
- [ ]* 6.7 Écrire le test de propriété de détermination de l'état de configuration du profil
|
||||||
|
- **Property 31 : Détermination de l'état de configuration du profil**
|
||||||
|
- **Validates: Requirements 16.1, 16.2**
|
||||||
|
|
||||||
|
- [x] 7. Checkpoint — S'assurer que tous les tests passent
|
||||||
|
- Ensure all tests pass, ask the user if questions arise.
|
||||||
|
|
||||||
|
- [x] 8. Service et API des trajets (CRUD + positions)
|
||||||
|
- [x] 8.1 Implémenter `TrajetRepository` et la gestion des positions
|
||||||
|
- Résolution du véhicule à la date du trajet et copie de la PuissanceFiscale (dénormalisation)
|
||||||
|
- Attribution de Position : fin de journée `max(Position)+1.0`, insertion `(P1+P2)/2`, renumérotation par pas de 1.0 si précision < 0.001 (transaction)
|
||||||
|
- _Requirements: 10.14, 11.7, 11.8_
|
||||||
|
|
||||||
|
- [x] 8.2 Implémenter `TrajetService` (création)
|
||||||
|
- Validation, association véhicule courant si VehiculeId absent, calcul de distance via `DistanceCalculatorService`, mise en cache des adresses résolues, persistance transactionnelle
|
||||||
|
- Gérer distance nulle + avertissement quand aucune coordonnée n'est résoluble
|
||||||
|
- _Requirements: 1.2, 1.3, 1.11, 2.4, 2.5, 6.3, 6.7, 6.8, 10.14_
|
||||||
|
|
||||||
|
- [ ]* 8.3 Écrire le test de propriété de précision des distances
|
||||||
|
- **Property 3 : Précision de stockage des distances**
|
||||||
|
- **Validates: Requirements 2.2, 2.5, 6.2**
|
||||||
|
|
||||||
|
- [ ]* 8.4 Écrire le test de propriété de persistance du commentaire
|
||||||
|
- **Property 13 : Persistance optionnelle du commentaire**
|
||||||
|
- **Validates: Requirements 1.11, 1.12**
|
||||||
|
|
||||||
|
- [x] 8.5 Implémenter `TrajetService` (modification, suppression, réordonnancement)
|
||||||
|
- Modification : recalcul distance uniquement si adresse modifiée, commentaire seul sans recalcul, rollback si recalcul échoue, 404 si inexistant
|
||||||
|
- Suppression transactionnelle ; `PATCH /api/trajets/reordonner` met à jour les Position sans altérer les autres champs
|
||||||
|
- _Requirements: 5.2, 5.3, 5.5, 5.6, 5.8, 5.9, 5.10, 6.7, 6.8, 11.9_
|
||||||
|
|
||||||
|
- [ ]* 8.6 Écrire le test de propriété de recalcul conditionnel
|
||||||
|
- **Property 6 : Recalcul conditionnel de la distance**
|
||||||
|
- **Validates: Requirements 5.2, 5.3**
|
||||||
|
|
||||||
|
- [ ]* 8.7 Écrire les tests de propriété sur les positions
|
||||||
|
- **Property 14 : Cohérence de l'ordre des trajets d'une journée**
|
||||||
|
- **Property 15 : Unicité et cohérence des positions au sein d'une journée**
|
||||||
|
- **Validates: Requirements 11.7, 11.8, 11.9**
|
||||||
|
|
||||||
|
- [x] 8.8 Implémenter `TrajetsController` et brancher le controller adresses
|
||||||
|
- `POST/GET/PUT/DELETE /api/trajets`, `PATCH /api/trajets/reordonner`, `GET /api/trajets/journee/{date}` (avec géométries), `GET /api/adresses/search`
|
||||||
|
- _Requirements: 1.3, 5.5, 5.6, 8.7, 9.3_
|
||||||
|
|
||||||
|
- [ ]* 8.9 Écrire le test de propriété d'invariant de suppression
|
||||||
|
- **Property 5 : Invariant de suppression**
|
||||||
|
- **Validates: Requirements 5.5, 6.4**
|
||||||
|
|
||||||
|
- [x] 9. Résumés, totaux et navigation temporelle
|
||||||
|
- [x] 9.1 Implémenter `ResumeService` (mensuel et annuel)
|
||||||
|
- Résumé mensuel filtré par véhicule, trié par date puis Position, total 2 décimales ; validation mois [1,12] et année [2000,2100]
|
||||||
|
- Résumé annuel : toujours 12 mois (0 si vide), total = somme des 12 mois
|
||||||
|
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.6, 3.8, 4.2, 4.3, 4.4, 4.7, 6.5, 6.6_
|
||||||
|
|
||||||
|
- [ ]* 9.2 Écrire les tests de propriété des totaux et résumés
|
||||||
|
- **Property 1 : Cohérence du total mensuel** — **Validates: Requirements 3.3, 6.5**
|
||||||
|
- **Property 2 : Cohérence du total annuel** — **Validates: Requirements 4.3, 6.6**
|
||||||
|
- **Property 7 : Mois toujours présents dans le résumé annuel** — **Validates: Requirements 4.2, 4.4**
|
||||||
|
- **Property 8 : Rejet des paramètres de résumé hors plage** — **Validates: Requirements 3.6**
|
||||||
|
|
||||||
|
- [x] 9.3 Implémenter `TotauxService` et la navigation temporelle
|
||||||
|
- Totaux jour/mois/Total_Annuel_Partiel filtrés par véhicule (cumul jusqu'au mois inclus, exclusion des mois postérieurs)
|
||||||
|
- Années disponibles (données + année courante), mois disponibles, jours disponibles
|
||||||
|
- _Requirements: 12.7, 12.8, 12.15, 12.16, 12.19_
|
||||||
|
|
||||||
|
- [ ]* 9.4 Écrire les tests de propriété des totaux filtrés et partiels
|
||||||
|
- **Property 17 : Total annuel partiel borné au mois sélectionné** — **Validates: Requirements 12.8**
|
||||||
|
- **Property 21 : Totaux filtrés par véhicule** — **Validates: Requirements 12.19**
|
||||||
|
|
||||||
|
- [x] 9.5 Implémenter `ResumesController` et les controllers de navigation
|
||||||
|
- `GET /api/resumes/mensuel/{annee}/{mois}`, `GET /api/resumes/annuel/{annee}`
|
||||||
|
- `GET /api/annees-disponibles`, `/api/mois-disponibles/{annee}`, `/api/jours-disponibles/{annee}/{mois}`, `/api/totaux/{date}`
|
||||||
|
- _Requirements: 3.8, 4.1, 12.15, 12.16_
|
||||||
|
|
||||||
|
- [x] 10. Barème kilométrique et régularisation
|
||||||
|
- [x] 10.1 Implémenter `BaremeService` et le controller barème
|
||||||
|
- CRUD des tranches, validation du non-chevauchement par (année, puissance fiscale)
|
||||||
|
- Sélection de tranche contenant le total annuel, formule `km × Coefficient + Base`, barème non configuré → montant zéro + avertissement
|
||||||
|
- `GET /api/bareme/{annee}`, `POST/PUT/DELETE /api/bareme`
|
||||||
|
- _Requirements: 13.1, 13.2, 13.3, 13.4_
|
||||||
|
|
||||||
|
- [ ]* 10.2 Écrire les tests de propriété de sélection de tranche
|
||||||
|
- **Property 24 : Sélection de tranche du barème** — **Validates: Requirements 13.3, 13.4**
|
||||||
|
- **Property 27 : Sélection de tranche par le total annuel** — **Validates: Requirements 13.3, 14.6**
|
||||||
|
|
||||||
|
- [x] 10.3 Implémenter `MontantRegleService` (upsert et montant prévisionnel)
|
||||||
|
- Upsert par (année, mois, véhicule), rejet montant négatif
|
||||||
|
- Montant_Previsionnel = 80% du barème sur le Total_Annuel_Partiel (indicatif, hors régularisation)
|
||||||
|
- _Requirements: 13.5, 13.6, 14.1, 14.2, 14.3, 14.4_
|
||||||
|
|
||||||
|
- [ ]* 10.4 Écrire les tests de propriété montants réglés / prévisionnel
|
||||||
|
- **Property 23 : Indication de versement à 80% (informative)** — **Validates: Requirements 13.5, 13.6**
|
||||||
|
- **Property 26 : Unicité du montant réglé par période et véhicule** — **Validates: Requirements 14.3**
|
||||||
|
|
||||||
|
- [x] 10.5 Implémenter `RemboursementService` (régularisation annuelle)
|
||||||
|
- Montant_Reel = barème plein 100% sur total annuel, Frais_Reel_Mensuel = km_mois × coef tranche annuelle, Regularisation = Montant_Reel − total réglé
|
||||||
|
- Construire `RegularisationAnnuelleDto` (12 lignes + totaux)
|
||||||
|
- `GET /api/montant-previsionnel/...`, `GET/PUT /api/montants-regles`, `GET /api/regularisation/{annee}`
|
||||||
|
- _Requirements: 14.5, 14.6, 14.7, 14.8_
|
||||||
|
|
||||||
|
- [ ]* 10.6 Écrire le test de propriété de régularisation
|
||||||
|
- **Property 25 : Cohérence de la régularisation annuelle (barème plein)**
|
||||||
|
- **Validates: Requirements 13.3, 14.5, 14.6, 14.7**
|
||||||
|
|
||||||
|
- [x] 11. Export PDF (QuestPDF)
|
||||||
|
- [x] 11.1 Implémenter `PdfService` — rapport mensuel
|
||||||
|
- En-tête : identité (nom, prénom, matricule), véhicule (plaque, puissance fiscale, coef/base tranche), période, adresse départ profil, totaux mois/année, TOTAL IK du mois, Montant_Regle
|
||||||
|
- Liste des trajets (date, motif/commentaire, départ → arrivée, km) ; timeout 15 s
|
||||||
|
- _Requirements: 7.1, 7.4, 7.6, 7.8, 7.10, 7.11_
|
||||||
|
|
||||||
|
- [x] 11.2 Implémenter `PdfService` — synthèse annuelle et rapports par véhicule
|
||||||
|
- Page synthèse (identité, année, domicile, barème de l'année, véhicule, tranche) + 12 lignes (km, réglé, frais réel) + totaux (km annuel, réglé, réel, régularisation)
|
||||||
|
- Génération d'un PDF distinct par véhicule si plusieurs véhicules sur la période
|
||||||
|
- _Requirements: 7.2, 7.4, 7.6, 7.7, 7.8, 7.9, 7.12_
|
||||||
|
|
||||||
|
- [x] 11.3 Implémenter `ExportsPdfController`
|
||||||
|
- `GET /api/exports/mensuel/{annee}/{mois}` et `GET /api/exports/annuel/{annee}` ; en-tête Content-Disposition `trajets_MM_AAAA.pdf` / `trajets_AAAA.pdf`, timeout → 503
|
||||||
|
- _Requirements: 7.3, 7.4, 7.5_
|
||||||
|
|
||||||
|
- [ ]* 11.4 Écrire le test de propriété de nommage PDF
|
||||||
|
- **Property 9 : Nommage des fichiers PDF exportés**
|
||||||
|
- **Validates: Requirements 7.4**
|
||||||
|
|
||||||
|
- [ ]* 11.5 Écrire les tests unitaires PDF
|
||||||
|
- En-tête identité/véhicule, PDF distinct par véhicule, montants mensuels, régularisation annuelle
|
||||||
|
- _Requirements: 7.6, 7.8, 7.9, 7.12_
|
||||||
|
|
||||||
|
- [x] 12. Checkpoint backend — S'assurer que tous les tests passent
|
||||||
|
- Ensure all tests pass, ask the user if questions arise.
|
||||||
|
|
||||||
|
- [ ]* 13. Tests d'intégration backend end-to-end
|
||||||
|
- [ ]* 13.1 Tests d'intégration trajets et distance (WebApplicationFactory + SQLite en mémoire)
|
||||||
|
- POST avec/sans coordonnées (BAN puis ORS, fallback Haversine, distance nulle + 201), insertion entre deux trajets, PATCH réordonner, PUT/DELETE
|
||||||
|
- _Requirements: 1.3, 2.4, 2.6, 5.5, 11.8_
|
||||||
|
|
||||||
|
- [ ]* 13.2 Tests d'intégration résumés, totaux, véhicules, barème et exports
|
||||||
|
- Résumés mensuel/annuel, totaux partiels, réaffectation accepter/refuser, barème chevauchant → 400, upsert montant réglé, régularisation, export PDF 2 véhicules → 2 fichiers
|
||||||
|
- _Requirements: 3.8, 4.1, 7.9, 10.16, 12.8, 13.2, 14.3, 14.7_
|
||||||
|
|
||||||
|
- [x] 14. Socle frontend Angular et services
|
||||||
|
- [x] 14.1 Initialiser l'application Angular et le socle core
|
||||||
|
- Créer l'app Angular, `app-routing.module.ts`, modèles TypeScript, `notification.service.ts`, `TimeoutInterceptor` (10 s), `DateJjMmAAAAValidator`
|
||||||
|
- _Requirements: 1.9, 1.5_
|
||||||
|
|
||||||
|
- [x] 14.2 Implémenter les services HTTP Angular
|
||||||
|
- `TrajetService`, `VehiculeService`, `BANService` (autocomplétion), `AdresseCacheService`, `ProfilService`, `BaremeService`, `RemboursementService`
|
||||||
|
- `ProfilService.getEtat()` : appelle `GET /api/profil/etat` et retourne `EtatProfilDto`
|
||||||
|
- _Requirements: 1.2, 9.3, 9.4, 16.2_
|
||||||
|
|
||||||
|
- [ ]* 14.3 Écrire les tests unitaires des services
|
||||||
|
- Mock HttpClient, comportement du TimeoutInterceptor
|
||||||
|
- _Requirements: 1.9_
|
||||||
|
|
||||||
|
- [x] 15. Saisie et édition des trajets (frontend)
|
||||||
|
- [x] 15.1 Implémenter `TrajetFormComponent`
|
||||||
|
- Reactive Form (date, adresses 1–255, commentaire 0–500), validations synchrones, mode création/édition/insertion, autocomplétion BAN dès 3 car. + stockage coordonnées, boutons « Utiliser mon domicile », avertissement saisie manuelle sans coordonnées
|
||||||
|
- _Requirements: 1.1, 1.4, 1.5, 1.8, 1.10, 1.13, 1.14, 1.15, 1.16, 1.17, 5.1, 10.4, 10.5, 10.6, 11.1, 11.2, 11.3, 11.4_
|
||||||
|
|
||||||
|
- [x] 15.2 Implémenter `TrajetListComponent`
|
||||||
|
- Liste triée par Position, actions modifier/supprimer (avec `ConfirmDialog`)/insérer après/insérer avant le suivant, drag & drop → `PATCH reordonner`, rafraîchissement < 1 000 ms, icône + tooltip commentaire
|
||||||
|
- _Requirements: 5.1, 5.4, 5.7, 11.6, 11.9_
|
||||||
|
|
||||||
|
- [ ]* 15.3 Écrire les tests des composants de saisie
|
||||||
|
- Validation, autocomplétion BAN, mode insertion pré-rempli, drag & drop, tri par Position
|
||||||
|
- _Requirements: 1.4, 1.5, 1.13, 11.1, 11.6_
|
||||||
|
|
||||||
|
- [x] 16. Consultation, navigation temporelle et carte (frontend)
|
||||||
|
- [x] 16.1 Implémenter `SelecteurTemporelComponent`
|
||||||
|
- Sélecteurs année/mois/jour avec surbrillance des périodes ayant des données, désactivation des dates futures
|
||||||
|
- _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5_
|
||||||
|
|
||||||
|
- [x] 16.2 Implémenter `PanneauTotauxComponent`
|
||||||
|
- Totaux jour/mois/Total_Annuel_Partiel à 2 décimales, Montant_Previsionnel 80% indicatif, recalcul au changement
|
||||||
|
- _Requirements: 12.7, 12.8, 13.5, 13.6, 13.7, 13.8_
|
||||||
|
|
||||||
|
- [x] 16.3 Implémenter `CarteTrajetsComponent`
|
||||||
|
- Leaflet + tuiles OpenStreetMap, tracés GeoJSON par trajet, fallback ligne droite, infobulles, zoom/déplacement, message si vide ou carte indisponible
|
||||||
|
- _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.8_
|
||||||
|
|
||||||
|
- [x] 16.4 Implémenter `ConsultationJourComponent` (layout principal)
|
||||||
|
- Deux colonnes (liste scrollable + panneau sticky totaux/carte), boutons jour ±1 (« suivant » désactivé si futur), sélecteur de véhicule filtrant liste/totaux/carte, rafraîchissement < 1 000 ms
|
||||||
|
- _Requirements: 12.6, 12.9, 12.10, 12.11, 12.12, 12.13, 12.14, 12.17, 12.18, 12.20_
|
||||||
|
|
||||||
|
- [ ]* 16.5 Écrire les tests des composants de consultation
|
||||||
|
- Surbrillance/dates futures, 3 totaux + prévisionnel, tracés carte + fallback, layout et filtrage par véhicule
|
||||||
|
- _Requirements: 8.2, 8.5, 12.5, 12.8, 12.18_
|
||||||
|
|
||||||
|
- [x] 17. Profil, véhicules, barème et montants réglés (frontend)
|
||||||
|
- [x] 17.1 Implémenter `ProfilComponent`, `VehiculesComponent` et `ReaffectationDialogComponent`
|
||||||
|
- Profil (nom/prénom/matricule 8 car./domicile) + avertissement géocodage ; historique véhicules trié, ajout avec DateEffet ≤ aujourd'hui, dialog de réaffectation (nombre de trajets, accepter/refuser)
|
||||||
|
- _Requirements: 10.1, 10.2, 10.8, 10.9, 10.10, 10.11, 10.15, 10.16, 10.17_
|
||||||
|
|
||||||
|
- [x] 17.2 Implémenter `BaremeComponent` et `MontantsReglesComponent`
|
||||||
|
- Saisie des 3 tranches par puissance fiscale/année avec détection de chevauchement ; saisie Montant_Regle par mois + prévisionnel indicatif, rejet négatif, montants 2 décimales
|
||||||
|
- _Requirements: 13.1, 13.7, 14.1, 14.4, 14.8_
|
||||||
|
|
||||||
|
- [x] 17.3 Implémenter `ResumeMensuelComponent` et `ResumeAnnuelComponent`
|
||||||
|
- Résumé mensuel (liste + total 2 décimales, bouton export), résumé annuel (12 lignes + total ≤ 999 999,99, bouton export), déclenchement téléchargement PDF + gestion d'erreur
|
||||||
|
- _Requirements: 3.1, 3.5, 3.7, 4.1, 4.5, 4.6, 7.3_
|
||||||
|
|
||||||
|
- [ ]* 17.4 Écrire les tests des composants profil/véhicules/barème/montants/résumés
|
||||||
|
- Matricule 8 car., date effet future bloquée, dialog réaffectation, chevauchement barème, montant négatif rejeté, affichage 2 décimales et max annuel
|
||||||
|
- _Requirements: 4.5, 10.2, 10.11, 13.1, 14.4_
|
||||||
|
|
||||||
|
- [x] 18. Checkpoint final — S'assurer que tous les tests passent
|
||||||
|
- Ensure all tests pass, ask the user if questions arise.
|
||||||
|
|
||||||
|
- [x] 19. Carnet d'adresses nommées (backend)
|
||||||
|
- [x] 19.1 Implémenter `AdresseNommeeService` et `AdressesNommeesController`
|
||||||
|
- CRUD des Adresses_Nommées ; normalisation (trim + minuscules + suppression des accents) pour l'unicité de la Dénomination et le matching du Cache_Adresses
|
||||||
|
- Réutilisation du Cache_Adresses si l'adresse normalisée y est présente (sans appel BAN) ; sinon géocodage/re-géocodage via BAN (timeout 5 s)
|
||||||
|
- Si BAN indisponible/timeout : persistance sans coordonnées + `GeocodageReussi = false` et réponse indiquant l'absence de coordonnées ; mise en cache des coordonnées obtenues
|
||||||
|
- Suppression sans impact sur les Trajets existants (conservation de leur adresse et coordonnées)
|
||||||
|
- Endpoints `GET/POST/PUT/DELETE /api/adresses-nommees`
|
||||||
|
- _Requirements: 15.1, 15.4, 15.5, 15.6, 15.7, 15.8, 15.9, 15.10, 15.11, 15.12, 15.14_
|
||||||
|
|
||||||
|
- [ ]* 19.2 Écrire le test de propriété d'unicité de la dénomination
|
||||||
|
- **Property 28 : Unicité normalisée de la dénomination**
|
||||||
|
- **Validates: Requirements 15.6, 15.7**
|
||||||
|
|
||||||
|
- [ ]* 19.3 Écrire le test de propriété de préservation des trajets à la suppression
|
||||||
|
- **Property 29 : Préservation des trajets à la suppression d'une adresse nommée**
|
||||||
|
- **Validates: Requirements 15.14**
|
||||||
|
|
||||||
|
- [ ]* 19.4 Écrire le test de propriété de réutilisation du cache et cohérence du géocodage
|
||||||
|
- **Property 30 : Réutilisation du cache et cohérence du géocodage des adresses nommées**
|
||||||
|
- **Validates: Requirements 15.9, 15.10, 15.11, 15.12**
|
||||||
|
|
||||||
|
- [x] 20. Carnet d'adresses nommées (frontend)
|
||||||
|
- [x] 20.1 Implémenter `AdresseNommeeService` (Angular) et `CarnetAdressesComponent`
|
||||||
|
- `AdresseNommeeService` : `getAdressesNommees`, `create`, `update`, `delete` (HTTP)
|
||||||
|
- `CarnetAdressesComponent` : liste triée par Dénomination, formulaire de création/modification avec autocomplétion BAN (min 3 car., debounce 300 ms, max 5 suggestions), confirmation de suppression, message de succès CRUD, avertissement si le géocodage a échoué
|
||||||
|
- _Requirements: 15.1, 15.2, 15.3, 15.4, 15.11, 15.13, 15.15_
|
||||||
|
|
||||||
|
- [x] 20.2 Étendre `TrajetFormComponent` avec le sélecteur d'Adresse_Nommee
|
||||||
|
- Pour les champs adresse de départ et adresse d'arrivée, ajouter un sélecteur d'Adresse_Nommee (par Dénomination) en mode création ET insertion
|
||||||
|
- À la sélection, remplir le champ + associer les coordonnées (remplace le contenu pré-rempli en mode insertion)
|
||||||
|
- _Requirements: 1.18, 1.19, 11.10, 11.11, 15.16_
|
||||||
|
|
||||||
|
- [ ]* 20.3 Écrire les tests des composants carnet et du sélecteur dans le formulaire
|
||||||
|
- Composant Carnet_Adresses (liste, formulaire, confirmation, message), sélecteur d'Adresse_Nommee dans `TrajetFormComponent`
|
||||||
|
- _Requirements: 15.2, 15.13, 1.19_
|
||||||
|
|
||||||
|
- [x] 21. Onboarding profil à la première utilisation (frontend)
|
||||||
|
- [x] 21.1 Implémenter `OnboardingComponent`
|
||||||
|
- Formulaire une étape : identité (nom, prénom, matricule), Adresse_Domicile optionnelle avec autocomplétion BAN, au moins un Véhicule (plaque, puissance fiscale, date d'effet ≤ aujourd'hui)
|
||||||
|
- Bouton « Commencer » désactivé tant que les champs requis manquent
|
||||||
|
- À l'enregistrement réussi du profil + véhicule → navigation vers la vue de consultation
|
||||||
|
- Gestion d'erreur de persistance : message d'erreur + conservation des données saisies
|
||||||
|
- _Requirements: 16.3, 16.5, 16.6, 16.7, 16.8, 16.9, 16.10, 16.12_
|
||||||
|
|
||||||
|
- [x] 21.2 Implémenter le guard `ProfilConfigureGuard` (CanActivate)
|
||||||
|
- Appelle `ProfilService.getEtat()` au chargement ; redirige vers `/onboarding` si `EstConfigure = false` et bloque l'accès aux routes trajets/consultation ; laisse passer si `true`
|
||||||
|
- Gère l'échec de récupération de l'état par un message de connexion + action « Réessayer »
|
||||||
|
- Déclarer le guard sur les routes protégées dans `app-routing`
|
||||||
|
- _Requirements: 16.3, 16.4, 16.11, 16.13_
|
||||||
|
|
||||||
|
- [ ]* 21.3 Écrire les tests du parcours d'onboarding et du guard
|
||||||
|
- Bouton « Commencer » désactivé, navigation après enregistrement, redirection du guard, cas `EstConfigure` true/false, erreur + retry
|
||||||
|
- _Requirements: 16.3, 16.8, 16.10, 16.11, 16.13_
|
||||||
|
|
||||||
|
- [x] 22. Conteneurisation et déploiement Docker Compose
|
||||||
|
- [x] 22.1 Écrire le `Dockerfile` backend multi-stage
|
||||||
|
- Étape build (SDK) → étape runtime (aspnet), exposition du port 8080
|
||||||
|
- Base SQLite sous le volume `/data/trajets.db` via `ConnectionStrings__Default`
|
||||||
|
- Application automatique des migrations EF Core au démarrage (`Database.MigrateAsync()` dans `Program.cs`)
|
||||||
|
- Endpoint de santé `GET /health` (`AddHealthChecks` / `MapHealthChecks`)
|
||||||
|
- _Requirements: 6.1_
|
||||||
|
|
||||||
|
- [x] 22.2 Écrire le `Dockerfile` frontend multi-stage et la configuration `nginx.conf`
|
||||||
|
- Étape build (node) → étape serve (nginx)
|
||||||
|
- `nginx.conf` : fallback SPA vers `index.html` + reverse-proxy `/api` vers `backend:8080`
|
||||||
|
|
||||||
|
- [x] 22.3 Écrire le `docker-compose.yml`
|
||||||
|
- Services backend + frontend, volume nommé `trajets-data:/data` pour la persistance SQLite
|
||||||
|
- Clé ORS via variable d'environnement `${ORS_API_KEY}` depuis un fichier `.env` non commité
|
||||||
|
- Mapping hôte 8080 → nginx, `depends_on: service_healthy`, `restart: unless-stopped`, healthcheck backend
|
||||||
|
- Ajouter `.env` au `.gitignore`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Les tâches marquées `*` sont optionnelles (tests unitaires, tests de propriété, tests d'intégration) et peuvent être ignorées pour un MVP plus rapide ; les tâches d'implémentation cœur ne le sont jamais.
|
||||||
|
- Chaque tâche référence des exigences précises pour la traçabilité ; les sous-tâches de test de propriété référencent explicitement la propriété de correction validée (numéro + exigences).
|
||||||
|
- Les 31 propriétés de correction sont couvertes par des tests FsCheck (backend) répartis au plus près de leur implémentation pour détecter les erreurs tôt.
|
||||||
|
- La clé ORS n'est jamais commitée : User Secrets en développement, variables d'environnement en production.
|
||||||
|
- Les checkpoints garantissent une validation incrémentale du backend puis du frontend.
|
||||||
|
|
||||||
|
## Task Dependency Graph
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"waves": [
|
||||||
|
{ "id": 0, "tasks": ["1"] },
|
||||||
|
{ "id": 1, "tasks": ["2.1"] },
|
||||||
|
{ "id": 2, "tasks": ["2.2", "3.1"] },
|
||||||
|
{ "id": 3, "tasks": ["2.3", "3.2", "4.1", "4.2"] },
|
||||||
|
{ "id": 4, "tasks": ["3.3", "4.3", "5.1", "6.1", "6.5"] },
|
||||||
|
{ "id": 5, "tasks": ["4.4", "4.5", "5.2", "5.3", "6.2", "6.3", "6.4", "6.6", "6.7", "8.1"] },
|
||||||
|
{ "id": 6, "tasks": ["8.2", "10.1", "19.1"] },
|
||||||
|
{ "id": 7, "tasks": ["8.3", "8.4", "8.5", "10.2", "10.3", "19.2", "19.3", "19.4"] },
|
||||||
|
{ "id": 8, "tasks": ["8.6", "8.7", "8.8", "10.4", "10.5"] },
|
||||||
|
{ "id": 9, "tasks": ["8.9", "9.1", "9.3", "10.6", "11.1", "11.2"] },
|
||||||
|
{ "id": 10, "tasks": ["9.2", "9.4", "9.5", "11.3"] },
|
||||||
|
{ "id": 11, "tasks": ["11.4", "11.5", "13.1", "13.2"] },
|
||||||
|
{ "id": 12, "tasks": ["14.1", "22.1"] },
|
||||||
|
{ "id": 13, "tasks": ["14.2"] },
|
||||||
|
{ "id": 14, "tasks": ["14.3", "15.1", "16.1", "16.3", "17.1", "20.1", "21.1"] },
|
||||||
|
{ "id": 15, "tasks": ["15.2", "16.2", "16.4", "17.2", "17.3", "20.2", "21.2", "22.2"] },
|
||||||
|
{ "id": 16, "tasks": ["15.3", "16.5", "17.4", "20.3", "21.3", "22.3"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,906 @@
|
|||||||
|
{
|
||||||
|
"pbtResults": {},
|
||||||
|
"executionHistory": {
|
||||||
|
"1. Initialiser la solution et le socle backend": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287232165
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287246543
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287998132
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"2.1 Définir les entités du domaine": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287232559
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288005627
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288114112
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"2.2 Configurer `AppDbContext` et les migrations": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287232721
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288119065
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288331297
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"3.1 Créer les DTOs et enregistrements": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287232879
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288119884
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288332271
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"3.2 Implémenter les validateurs FluentValidation": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287233063
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288338632
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288511176
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"4.1 Implémenter `IBANClient` / `BANClient`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287233177
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288339629
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288512549
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"4.2 Implémenter `IORSClient` / `ORSClient`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287233302
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288340752
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288513521
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"4.3 Implémenter `IDistanceCalculator` / `DistanceCalculatorService`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287233400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288541576
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288751981
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"5.1 Implémenter `AdresseCacheService` et le repository associé": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287233556
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288542488
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288752928
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"6.1 Implémenter `VehiculeService` et le repository véhicules": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287233727
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288543893
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288754864
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"6.5 Implémenter `ProfilService` et le controller `ProfilController`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287233834
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288544845
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288922998
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"7. Checkpoint — S'assurer que tous les tests passent": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287233946
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288928483
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288949593
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"8.1 Implémenter `TrajetRepository` et la gestion des positions": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287234050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288963408
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789289192036
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"8.2 Implémenter `TrajetService` (création)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287234243
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789289199983
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789289530614
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"8.5 Implémenter `TrajetService` (modification, suppression, réordonnancement)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287234350
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789289537094
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290182288
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"8.8 Implémenter `TrajetsController` et brancher le controller adresses": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287234531
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290188266
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290381932
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"9.1 Implémenter `ResumeService` (mensuel et annuel)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287234746
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290189218
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290380584
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"9.3 Implémenter `TotauxService` et la navigation temporelle": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287234874
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290190414
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290382822
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"9.5 Implémenter `ResumesController` et les controllers de navigation": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287234983
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290389151
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290611560
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"10.1 Implémenter `BaremeService` et le controller barème": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287235093
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288964381
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789289189697
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"10.3 Implémenter `MontantRegleService` (upsert et montant prévisionnel)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287235202
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789289200933
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789289531613
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"10.5 Implémenter `RemboursementService` (régularisation annuelle)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287235303
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789289538235
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290181307
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"11.1 Implémenter `PdfService` — rapport mensuel": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287235417
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290389978
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290610124
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"11.2 Implémenter `PdfService` — synthèse annuelle et rapports par véhicule": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287235641
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290612582
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290840695
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"11.3 Implémenter `ExportsPdfController`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287235735
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290841795
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290955479
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"12. Checkpoint backend — S'assurer que tous les tests passent": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287235836
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290960674
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290981616
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"14.1 Initialiser l'application Angular et le socle core": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287235947
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290991997
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789292473439
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"14.2 Implémenter les services HTTP Angular": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287236050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789292479753
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789292716818
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"15.1 Implémenter `TrajetFormComponent`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287236150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789292723557
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789293936331
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"15.2 Implémenter `TrajetListComponent`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287236250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294145233
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294848699
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"16.1 Implémenter `SelecteurTemporelComponent`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287236348
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789292725073
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789293937718
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"16.2 Implémenter `PanneauTotauxComponent`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287236452
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294146289
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294849892
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"16.3 Implémenter `CarteTrajetsComponent`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287236569
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789292725912
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789293939158
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"16.4 Implémenter `ConsultationJourComponent` (layout principal)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287236668
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294856111
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789295779877
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"17.1 Implémenter `ProfilComponent`, `VehiculesComponent` et `ReaffectationDialogComponent`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287236781
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789292727318
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789293940464
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"17.2 Implémenter `BaremeComponent` et `MontantsReglesComponent`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287236901
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294147594
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294847717
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"17.3 Implémenter `ResumeMensuelComponent` et `ResumeAnnuelComponent`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287237023
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294149107
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294845080
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"18. Checkpoint final — S'assurer que tous les tests passent": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287237181
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789295909781
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789295952171
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"19.1 Implémenter `AdresseNommeeService` et `AdressesNommeesController`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287237302
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288965614
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789289191066
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"20.1 Implémenter `AdresseNommeeService` (Angular) et `CarnetAdressesComponent`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287237403
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789292728418
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789293941575
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"20.2 Étendre `TrajetFormComponent` avec le sélecteur d'Adresse_Nommee": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287237505
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294150157
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294846331
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"21.1 Implémenter `OnboardingComponent`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287237623
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789293946937
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294136345
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"21.2 Implémenter le guard `ProfilConfigureGuard` (CanActivate)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287237734
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294857367
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789295904278
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"22.1 Écrire le `Dockerfile` backend multi-stage": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287237834
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290993096
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789292474299
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"22.2 Écrire le `Dockerfile` frontend multi-stage et la configuration `nginx.conf`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287237949
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789293948068
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294137486
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"22.3 Écrire le `docker-compose.yml`": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789287238070
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294858181
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789295780772
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"2. Modèle de données et contexte EF Core": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288331545
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"3. DTOs, options de configuration et validateurs": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288511390
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"4. Intégrations externes : BAN et ORS": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288752247
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"5. Cache des adresses géocodées": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288753060
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"6. Gestion des véhicules et du profil": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789288923258
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"19. Carnet d'adresses nommées (backend)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789289191199
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"10. Barème kilométrique et régularisation": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290181589
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"8. Service et API des trajets (CRUD + positions)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290382197
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"9. Résumés, totaux et navigation temporelle": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290611752
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"11. Export PDF (QuestPDF)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789290955735
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"14. Socle frontend Angular et services": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789292717019
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"20. Carnet d'adresses nommées (frontend)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294846578
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"17. Profil, véhicules, barème et montants réglés (frontend)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294847882
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"15. Saisie et édition des trajets (frontend)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789294848965
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"16. Consultation, navigation temporelle et carte (frontend)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789295780126
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"22. Conteneurisation et déploiement Docker Compose": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789295781029
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"21. Onboarding profil à la première utilisation (frontend)": [
|
||||||
|
{
|
||||||
|
"executionId": "ee6b2d0f-d644-4cba-8c61-09b790014d2d",
|
||||||
|
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
|
||||||
|
"timestamp": 1789295904483
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Build output
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|
||||||
|
# SQLite database files
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
secrets.json
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# ---- Étape build (SDK) ----
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY *.sln ./
|
||||||
|
COPY TrajetKmTracker.Api/*.csproj TrajetKmTracker.Api/
|
||||||
|
RUN dotnet restore TrajetKmTracker.Api/TrajetKmTracker.Api.csproj
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet publish TrajetKmTracker.Api/TrajetKmTracker.Api.csproj -c Release -o /app/publish
|
||||||
|
|
||||||
|
# ---- Étape runtime (ASP.NET) ----
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# curl est requis pour le HEALTHCHECK Docker (l'image aspnet ne l'inclut pas par défaut).
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
|
||||||
|
# Répertoire du volume pour la base SQLite persistée
|
||||||
|
RUN mkdir -p /data
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
# La base SQLite est stockée sur le volume monté ; le chemin est fourni via la chaîne de connexion
|
||||||
|
ENV ConnectionStrings__Default="Data Source=/data/trajets.db"
|
||||||
|
ENV ASPNETCORE_URLS="http://+:8080"
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Sonde de santé intégrée à l'image (utilisée aussi par Docker Compose).
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=20s \
|
||||||
|
CMD curl -f http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "TrajetKmTracker.Api.dll"]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Configuration;
|
||||||
|
|
||||||
|
/// <summary>Options de configuration de l'API BAN (Base Adresses Nationale).</summary>
|
||||||
|
public class BANOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "BAN";
|
||||||
|
|
||||||
|
public string BaseUrl { get; set; } = "https://api-adresse.data.gouv.fr";
|
||||||
|
public int TimeoutSeconds { get; set; } = 5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Configuration;
|
||||||
|
|
||||||
|
/// <summary>Options de configuration de l'API OpenRouteService (ORS).</summary>
|
||||||
|
public class ORSOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "ORS";
|
||||||
|
|
||||||
|
public string BaseUrl { get; set; } = string.Empty;
|
||||||
|
public string ApiKey { get; set; } = string.Empty;
|
||||||
|
public int TimeoutSeconds { get; set; } = 10;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoint d'autocomplétion locale des adresses déjà géocodées (Cache_Adresses).
|
||||||
|
/// La recherche exige au moins 3 caractères ; en deçà, une liste vide est retournée
|
||||||
|
/// (Exigence 9.3).
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/adresses")]
|
||||||
|
public class AdressesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IAdresseCacheService _adresseCacheService;
|
||||||
|
|
||||||
|
public AdressesController(IAdresseCacheService adresseCacheService)
|
||||||
|
{
|
||||||
|
_adresseCacheService = adresseCacheService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recherche dans le Cache_Adresses les suggestions correspondant au fragment <paramref name="q"/>
|
||||||
|
/// (min. 3 caractères), ordonnées de la plus récemment utilisée à la plus ancienne.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("search")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<AdresseCacheDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<AdresseCacheDto>>> Search(
|
||||||
|
[FromQuery] string? q,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var suggestions = await _adresseCacheService.RechercherLocalAsync(
|
||||||
|
q ?? string.Empty,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return Ok(suggestions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoints de gestion du Carnet_Adresses : liste, création, modification et suppression
|
||||||
|
/// des Adresses_Nommées (Exigence 15).
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/adresses-nommees")]
|
||||||
|
public class AdressesNommeesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IAdresseNommeeService _service;
|
||||||
|
|
||||||
|
public AdressesNommeesController(IAdresseNommeeService service)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Liste toutes les Adresses_Nommées, triées par Dénomination (15.1).</summary>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<AdresseNommeeDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<AdresseNommeeDto>>> GetAll(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var adresses = await _service.GetAllAsync(cancellationToken);
|
||||||
|
return Ok(adresses);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crée une Adresse_Nommee. Rejette une Dénomination invalide/dupliquée ou une adresse
|
||||||
|
/// invalide (400).
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(AdresseNommeeDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<AdresseNommeeDto>> Create(
|
||||||
|
[FromBody] SaveAdresseNommeeDto dto,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cree = await _service.CreateAsync(dto, cancellationToken);
|
||||||
|
return CreatedAtAction(nameof(GetAll), null, cree);
|
||||||
|
}
|
||||||
|
catch (DenominationInvalideException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
catch (DenominationDupliqueeException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
catch (AdresseNommeeInvalideException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modifie une Adresse_Nommee. Rejette une Dénomination invalide/dupliquée ou une adresse
|
||||||
|
/// invalide (400) et une Adresse_Nommee introuvable (404).
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{id:int}")]
|
||||||
|
[ProducesResponseType(typeof(AdresseNommeeDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<AdresseNommeeDto>> Update(
|
||||||
|
int id,
|
||||||
|
[FromBody] SaveAdresseNommeeDto dto,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var maj = await _service.UpdateAsync(id, dto, cancellationToken);
|
||||||
|
return Ok(maj);
|
||||||
|
}
|
||||||
|
catch (AdresseNommeeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
catch (DenominationInvalideException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
catch (DenominationDupliqueeException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
catch (AdresseNommeeInvalideException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supprime une Adresse_Nommee. Les Trajets existants restent inchangés (15.14).
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _service.DeleteAsync(id, cancellationToken);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (AdresseNommeeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoints de gestion du barème kilométrique : consultation par année et CRUD des tranches.
|
||||||
|
/// La validation du non-chevauchement des tranches ([KmMin, KmMax]) par (année, puissance fiscale)
|
||||||
|
/// est appliquée à l'ajout et à la modification (Exigences 13.1–13.2).
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/bareme")]
|
||||||
|
public class BaremeController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IBaremeService _baremeService;
|
||||||
|
|
||||||
|
public BaremeController(IBaremeService baremeService)
|
||||||
|
{
|
||||||
|
_baremeService = baremeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Retourne toutes les tranches du barème pour l'année indiquée.</summary>
|
||||||
|
[HttpGet("{annee:int}")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<TrancheBaremeDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<TrancheBaremeDto>>> GetBareme(
|
||||||
|
int annee, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tranches = await _baremeService.GetBaremeAsync(annee, cancellationToken);
|
||||||
|
return Ok(tranches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ajoute une tranche du barème. Rejette (400) si l'intervalle chevauche une tranche existante
|
||||||
|
/// pour la même (année, puissance fiscale).
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(TrancheBaremeDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<TrancheBaremeDto>> AddTranche(
|
||||||
|
[FromBody] SaveTrancheBaremeDto dto, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tranche = await _baremeService.AddTrancheAsync(dto, cancellationToken);
|
||||||
|
return CreatedAtAction(nameof(GetBareme), new { annee = tranche.Annee }, tranche);
|
||||||
|
}
|
||||||
|
catch (TranchesBaremeChevauchantesException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Met à jour une tranche existante. Rejette (400) en cas de chevauchement, 404 si la tranche est absente.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{id:int}")]
|
||||||
|
[ProducesResponseType(typeof(TrancheBaremeDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<TrancheBaremeDto>> UpdateTranche(
|
||||||
|
int id, [FromBody] SaveTrancheBaremeDto dto, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tranche = await _baremeService.UpdateTrancheAsync(id, dto, cancellationToken);
|
||||||
|
return Ok(tranche);
|
||||||
|
}
|
||||||
|
catch (TrancheBaremeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
catch (TranchesBaremeChevauchantesException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Années pour lesquelles un barème (au moins une tranche) est défini, triées croissantes.</summary>
|
||||||
|
[HttpGet("annees-disponibles")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<int>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<int>>> GetAnneesDisponibles(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var annees = await _baremeService.GetAnneesAvecBaremeAsync(cancellationToken);
|
||||||
|
return Ok(annees);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copie le dernier barème connu (année la plus récente antérieure ayant un barème) vers
|
||||||
|
/// l'année cible. Remplace les tranches existantes de l'année cible (annule et remplace).
|
||||||
|
/// Retourne le nombre de tranches copiées (0 si aucun barème antérieur n'existe).
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("copier/{anneeCible:int}")]
|
||||||
|
[ProducesResponseType(typeof(CopieBaremeResultDto), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<CopieBaremeResultDto>> CopierDernierBareme(
|
||||||
|
int anneeCible, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var nombre = await _baremeService.CopierDernierBaremeAsync(anneeCible, cancellationToken);
|
||||||
|
return Ok(new CopieBaremeResultDto(nombre));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Supprime une tranche du barème. Retourne 404 si la tranche est absente.</summary>
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> DeleteTranche(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _baremeService.DeleteTrancheAsync(id, cancellationToken);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (TrancheBaremeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
using System.Net.Mime;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoints d'Export_PDF : rapport mensuel et synthèse annuelle (Exigences 7.3, 7.4, 7.5).
|
||||||
|
/// Chaque réponse renvoie un unique fichier <c>application/pdf</c> avec l'en-tête
|
||||||
|
/// <c>Content-Disposition: attachment</c> et le nom de fichier suggéré par le service
|
||||||
|
/// (<c>trajets_MM_AAAA.pdf</c> pour le mensuel, <c>trajets_AAAA.pdf</c> pour l'annuel).
|
||||||
|
///
|
||||||
|
/// Le filtre par véhicule est optionnel : si <c>vehiculeId</c> est omis, le Véhicule_Courant
|
||||||
|
/// est utilisé par défaut.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Note (Exigence 7.9) : une réponse HTTP unique ne peut transporter qu'un seul fichier.
|
||||||
|
/// Ces endpoints produisent donc le rapport du véhicule sélectionné (ou courant). La génération
|
||||||
|
/// d'un rapport distinct par véhicule lorsqu'une période comporte plusieurs véhicules est assurée
|
||||||
|
/// par les variantes de service <c>GenererRapports…ParVehiculeAsync</c>, réservées à un usage
|
||||||
|
/// multi-fichiers ultérieur (p. ex. archive). Ici, un appel = un fichier filtré par véhicule.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Erreurs : un dépassement du délai de génération ou un échec de rendu lève une
|
||||||
|
/// <see cref="PdfGenerationException"/> mappée vers <c>503 Service Unavailable</c> ; un véhicule
|
||||||
|
/// introuvable lève une <see cref="VehiculeNotFoundException"/> mappée vers <c>404 Not Found</c>
|
||||||
|
/// (réponses ProblemDetails).
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/exports")]
|
||||||
|
public class ExportsPdfController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IPdfService _pdfService;
|
||||||
|
private readonly IVehiculeService _vehiculeService;
|
||||||
|
|
||||||
|
public ExportsPdfController(IPdfService pdfService, IVehiculeService vehiculeService)
|
||||||
|
{
|
||||||
|
_pdfService = pdfService;
|
||||||
|
_vehiculeService = vehiculeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Génère et télécharge le rapport PDF mensuel d'un (année, mois) pour le véhicule indiqué
|
||||||
|
/// (Véhicule_Courant si <paramref name="vehiculeId"/> est omis). Retourne les octets du PDF
|
||||||
|
/// avec <c>Content-Type: application/pdf</c> et <c>Content-Disposition: attachment;
|
||||||
|
/// filename="trajets_MM_AAAA.pdf"</c> (Exigences 7.3, 7.4).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("mensuel/{annee:int}/{mois:int}")]
|
||||||
|
[Produces(MediaTypeNames.Application.Pdf)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
public async Task<IActionResult> ExporterMensuel(
|
||||||
|
int annee, int mois, [FromQuery] int? vehiculeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vehicule = await ResoudreVehiculeIdAsync(vehiculeId, cancellationToken);
|
||||||
|
var pdf = await _pdfService.GenererRapportMensuelAsync(annee, mois, vehicule, cancellationToken);
|
||||||
|
return FichierPdf(pdf);
|
||||||
|
}
|
||||||
|
catch (VehiculeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
catch (PdfGenerationException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Génère et télécharge la synthèse PDF annuelle d'une année pour le véhicule indiqué
|
||||||
|
/// (Véhicule_Courant si <paramref name="vehiculeId"/> est omis). Retourne les octets du PDF
|
||||||
|
/// avec <c>Content-Type: application/pdf</c> et <c>Content-Disposition: attachment;
|
||||||
|
/// filename="trajets_AAAA.pdf"</c> (Exigences 7.3, 7.4).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("annuel/{annee:int}")]
|
||||||
|
[Produces(MediaTypeNames.Application.Pdf)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
public async Task<IActionResult> ExporterAnnuel(
|
||||||
|
int annee, [FromQuery] int? vehiculeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vehicule = await ResoudreVehiculeIdAsync(vehiculeId, cancellationToken);
|
||||||
|
var pdf = await _pdfService.GenererRapportAnnuelAsync(annee, vehicule, cancellationToken);
|
||||||
|
return FichierPdf(pdf);
|
||||||
|
}
|
||||||
|
catch (VehiculeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
catch (PdfGenerationException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Résout l'identifiant du véhicule à utiliser : celui fourni s'il est présent, sinon
|
||||||
|
/// le Véhicule_Courant (Date_Effet la plus récente <= aujourd'hui).
|
||||||
|
/// </summary>
|
||||||
|
private async Task<int> ResoudreVehiculeIdAsync(int? vehiculeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (vehiculeId.HasValue)
|
||||||
|
{
|
||||||
|
return vehiculeId.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var courant = await _vehiculeService.GetVehiculeCourantAsync(cancellationToken);
|
||||||
|
return courant.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renvoie le PDF en pièce jointe téléchargeable, en s'appuyant sur le nom de fichier
|
||||||
|
/// suggéré par le <see cref="PdfResult"/> (<c>Content-Disposition: attachment</c>).
|
||||||
|
/// </summary>
|
||||||
|
private FileContentResult FichierPdf(PdfResult pdf)
|
||||||
|
=> File(pdf.Contenu, MediaTypeNames.Application.Pdf, pdf.NomFichier);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoints de navigation temporelle et de totaux du Panneau_Totaux :
|
||||||
|
/// années/mois/jours disponibles (périodes ayant des données) et totaux
|
||||||
|
/// jour/mois/année partiel pour une date donnée (Exigences 12.15, 12.16).
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api")]
|
||||||
|
public class NavigationController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ITotauxService _totauxService;
|
||||||
|
|
||||||
|
public NavigationController(ITotauxService totauxService)
|
||||||
|
{
|
||||||
|
_totauxService = totauxService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne les années comportant au moins un Trajet, complétées par l'année
|
||||||
|
/// courante, triées croissantes (Exigence 12.15).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("annees-disponibles")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<int>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<int>>> GetAnneesDisponibles(
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var annees = await _totauxService.GetAnneesDisponiblesAsync(cancellationToken);
|
||||||
|
return Ok(annees);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne les mois (1..12) de l'année comportant au moins un Trajet,
|
||||||
|
/// triés croissants (Exigence 12.16).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("mois-disponibles/{annee:int}")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<int>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<int>>> GetMoisDisponibles(
|
||||||
|
int annee, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var mois = await _totauxService.GetMoisDisponiblesAsync(annee, cancellationToken);
|
||||||
|
return Ok(mois);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne les jours du mois de l'année comportant au moins un Trajet,
|
||||||
|
/// triés croissants (Exigence 12.16).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("jours-disponibles/{annee:int}/{mois:int}")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<int>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<int>>> GetJoursDisponibles(
|
||||||
|
int annee, int mois, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var jours = await _totauxService.GetJoursDisponiblesAsync(annee, mois, cancellationToken);
|
||||||
|
return Ok(jours);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne les totaux jour/mois/année partiel du Panneau_Totaux pour la date
|
||||||
|
/// indiquée et le véhicule (Véhicule_Courant si <paramref name="vehiculeId"/> omis).
|
||||||
|
/// La date est attendue au format ISO <c>yyyy-MM-dd</c> ; un format invalide est
|
||||||
|
/// rejeté avec un code HTTP 400.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("totaux/{date}")]
|
||||||
|
[ProducesResponseType(typeof(TotauxDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<TotauxDto>> GetTotaux(
|
||||||
|
string date, [FromQuery] int? vehiculeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!DateOnly.TryParseExact(date, "yyyy-MM-dd", CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.None, out var jour))
|
||||||
|
{
|
||||||
|
return Problem(
|
||||||
|
detail: $"La date '{date}' est invalide. Format attendu : yyyy-MM-dd.",
|
||||||
|
statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
var totaux = await _totauxService.GetTotauxAsync(jour, vehiculeId, cancellationToken);
|
||||||
|
return Ok(totaux);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoints de gestion du profil utilisateur (singleton) et de son état de configuration.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/profil")]
|
||||||
|
public class ProfilController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IProfilService _profilService;
|
||||||
|
|
||||||
|
public ProfilController(IProfilService profilService)
|
||||||
|
{
|
||||||
|
_profilService = profilService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Récupère le profil (créé à la volée s'il est absent).</summary>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(ProfilDto), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<ProfilDto>> GetProfil(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var profil = await _profilService.GetProfilAsync(cancellationToken);
|
||||||
|
return Ok(profil);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sauvegarde le profil. La validation du matricule (≤ 8 car.) est assurée par FluentValidation.
|
||||||
|
/// Le géocodage de l'Adresse_Domicile est effectué au mieux : un échec BAN n'empêche pas la sauvegarde.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut]
|
||||||
|
[ProducesResponseType(typeof(ProfilDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<ProfilDto>> SaveProfil(
|
||||||
|
[FromBody] SaveProfilDto dto,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var profil = await _profilService.SaveProfilAsync(dto, cancellationToken);
|
||||||
|
return Ok(profil);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>État de configuration du profil (onboarding).</summary>
|
||||||
|
[HttpGet("etat")]
|
||||||
|
[ProducesResponseType(typeof(EtatProfilDto), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<EtatProfilDto>> GetEtat(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var etat = await _profilService.GetEtatAsync(cancellationToken);
|
||||||
|
return Ok(etat);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoints « Barème et remboursements » : montant prévisionnel (80% indicatif),
|
||||||
|
/// montants réglés (saisie/consultation) et régularisation annuelle (barème plein).
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api")]
|
||||||
|
public class RemboursementController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMontantRegleService _montantRegleService;
|
||||||
|
private readonly IRemboursementService _remboursementService;
|
||||||
|
|
||||||
|
public RemboursementController(
|
||||||
|
IMontantRegleService montantRegleService,
|
||||||
|
IRemboursementService remboursementService)
|
||||||
|
{
|
||||||
|
_montantRegleService = montantRegleService;
|
||||||
|
_remboursementService = remboursementService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne le Montant_Previsionnel indicatif (80% du barème plein sur le total annuel partiel)
|
||||||
|
/// pour un (année, mois, véhicule). Purement informatif — hors régularisation (Exigences 13.5/13.6).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("montant-previsionnel/{annee:int}/{mois:int}")]
|
||||||
|
[ProducesResponseType(typeof(MontantPrevisionnelDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<MontantPrevisionnelDto>> GetMontantPrevisionnel(
|
||||||
|
int annee, int mois, [FromQuery] int vehiculeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dto = await _montantRegleService.GetMontantPrevisionnelAsync(
|
||||||
|
annee, mois, vehiculeId, cancellationToken);
|
||||||
|
return Ok(dto);
|
||||||
|
}
|
||||||
|
catch (VehiculeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne les Montant_Regle saisis pour une (année, véhicule), triés par mois croissant.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("montants-regles/{annee:int}")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<MontantRegleDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<MontantRegleDto>>> GetMontantsRegles(
|
||||||
|
int annee, [FromQuery] int vehiculeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var montants = await _montantRegleService.GetMontantsReglesAsync(annee, vehiculeId, cancellationToken);
|
||||||
|
return Ok(montants);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enregistre ou met à jour (upsert) un Montant_Regle par (année, mois, véhicule).
|
||||||
|
/// Rejette un montant négatif avec un code HTTP 400 (Exigence 14.4).
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("montants-regles")]
|
||||||
|
[ProducesResponseType(typeof(MontantRegleDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<MontantRegleDto>> UpsertMontantRegle(
|
||||||
|
[FromBody] MontantRegleDto dto, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _montantRegleService.UpsertMontantRegleAsync(dto, cancellationToken);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (MontantRegleNegatifException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne la synthèse annuelle de régularisation (barème plein) pour une (année, véhicule) :
|
||||||
|
/// tranche annuelle, 12 lignes mensuelles et totaux (km annuel, réglé, Montant_Reel, Regularisation).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("regularisation/{annee:int}")]
|
||||||
|
[ProducesResponseType(typeof(RegularisationAnnuelleDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<RegularisationAnnuelleDto>> GetRegularisation(
|
||||||
|
int annee, [FromQuery] int vehiculeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dto = await _remboursementService.GetRegularisationAnnuelleAsync(
|
||||||
|
annee, vehiculeId, cancellationToken);
|
||||||
|
return Ok(dto);
|
||||||
|
}
|
||||||
|
catch (VehiculeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoints des résumés kilométriques : Résumé_Mensuel et Résumé_Annuel.
|
||||||
|
/// Le filtre par véhicule est optionnel (Véhicule_Courant utilisé si omis).
|
||||||
|
/// Les paramètres hors bornes (mois ∉ [1, 12], année ∉ [2000, 2100]) sont
|
||||||
|
/// rejetés avec un code HTTP 400 (Exigences 3.6, 3.8, 4.1).
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/resumes")]
|
||||||
|
public class ResumesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IResumeService _resumeService;
|
||||||
|
|
||||||
|
public ResumesController(IResumeService resumeService)
|
||||||
|
{
|
||||||
|
_resumeService = resumeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne le Résumé_Mensuel d'un (année, mois) pour le véhicule indiqué
|
||||||
|
/// (Véhicule_Courant si <paramref name="vehiculeId"/> est omis). Rejette (400)
|
||||||
|
/// les paramètres hors bornes (Exigences 3.6, 3.8).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("mensuel/{annee:int}/{mois:int}")]
|
||||||
|
[ProducesResponseType(typeof(ResumeMensuelDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<ResumeMensuelDto>> GetResumeMensuel(
|
||||||
|
int annee, int mois, [FromQuery] int? vehiculeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resume = await _resumeService.GetResumeMensuelAsync(annee, mois, vehiculeId, cancellationToken);
|
||||||
|
return Ok(resume);
|
||||||
|
}
|
||||||
|
catch (ResumeParametreInvalideException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne le Résumé_Annuel d'une année (12 résumés mensuels) pour le véhicule
|
||||||
|
/// indiqué (Véhicule_Courant si <paramref name="vehiculeId"/> est omis). Rejette (400)
|
||||||
|
/// une année hors bornes (Exigences 3.6, 4.1).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("annuel/{annee:int}")]
|
||||||
|
[ProducesResponseType(typeof(ResumeAnnuelDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<ResumeAnnuelDto>> GetResumeAnnuel(
|
||||||
|
int annee, [FromQuery] int? vehiculeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resume = await _resumeService.GetResumeAnnuelAsync(annee, vehiculeId, cancellationToken);
|
||||||
|
return Ok(resume);
|
||||||
|
}
|
||||||
|
catch (ResumeParametreInvalideException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Repositories;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoints CRUD des Trajets, réordonnancement manuel et récupération des trajets d'une
|
||||||
|
/// journée enrichis des coordonnées et géométries issues du Cache_Adresses (Exigences 1.3,
|
||||||
|
/// 5.5, 5.6, 8.7, 9.3).
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/trajets")]
|
||||||
|
public class TrajetsController : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// En-tête de réponse portant l'avertissement lorsqu'un Trajet a été enregistré sans
|
||||||
|
/// distance calculable (Exigence 2.4). Le corps reste le <see cref="TrajetDto"/> attendu.
|
||||||
|
/// </summary>
|
||||||
|
private const string WarningHeader = "X-Trajet-Warning";
|
||||||
|
|
||||||
|
private readonly ITrajetService _trajetService;
|
||||||
|
private readonly ITrajetRepository _trajetRepository;
|
||||||
|
private readonly IAdresseCacheService _adresseCacheService;
|
||||||
|
|
||||||
|
public TrajetsController(
|
||||||
|
ITrajetService trajetService,
|
||||||
|
ITrajetRepository trajetRepository,
|
||||||
|
IAdresseCacheService adresseCacheService)
|
||||||
|
{
|
||||||
|
_trajetService = trajetService;
|
||||||
|
_trajetRepository = trajetRepository;
|
||||||
|
_adresseCacheService = adresseCacheService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crée un Trajet (Exigence 1.3). Retourne 201 + <see cref="TrajetDto"/>. Si la distance n'a
|
||||||
|
/// pas pu être calculée faute de coordonnées, la réponse reste 201 et un avertissement est
|
||||||
|
/// ajouté dans l'en-tête <c>X-Trajet-Warning</c> (Exigence 2.4).
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(TrajetDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<TrajetDto>> Create(
|
||||||
|
[FromBody] CreateTrajetDto dto,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resultat = await _trajetService.CreateAsync(dto, cancellationToken);
|
||||||
|
|
||||||
|
if (resultat.Avertissement && !string.IsNullOrWhiteSpace(resultat.Message))
|
||||||
|
{
|
||||||
|
Response.Headers[WarningHeader] = resultat.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreatedAtAction(
|
||||||
|
nameof(GetById),
|
||||||
|
new { id = resultat.Trajet.Id },
|
||||||
|
resultat.Trajet);
|
||||||
|
}
|
||||||
|
catch (VehiculeNotFoundException ex)
|
||||||
|
{
|
||||||
|
// Aucun véhicule applicable à la date et aucun VehiculeId fourni.
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recalcule la distance de TOUS les trajets via OpenRouteService (BAN + ORS), indépendamment
|
||||||
|
/// d'un changement d'adresse. Sert à corriger des distances calculées en mode dégradé (ex. clé
|
||||||
|
/// ORS invalide → distances à vol d'oiseau) une fois ORS de nouveau opérationnel.
|
||||||
|
/// Retourne 200 + le nombre de trajets recalculés.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("recalculer-distances")]
|
||||||
|
[ProducesResponseType(typeof(RecalculDistancesResultDto), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<RecalculDistancesResultDto>> RecalculerDistances(
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var nombre = await _trajetService.RecalculerToutesLesDistancesAsync(cancellationToken);
|
||||||
|
return Ok(new RecalculDistancesResultDto(nombre));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Récupère un Trajet par identifiant. 404 s'il est introuvable.</summary>
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
[ProducesResponseType(typeof(TrajetDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<TrajetDto>> GetById(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var trajet = await _trajetRepository.GetByIdAsync(id, cancellationToken);
|
||||||
|
if (trajet is null)
|
||||||
|
{
|
||||||
|
return Problem(
|
||||||
|
detail: $"Le trajet {id} n'existe pas.",
|
||||||
|
statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new TrajetDto(
|
||||||
|
trajet.Id,
|
||||||
|
trajet.Date,
|
||||||
|
trajet.AdresseDepart,
|
||||||
|
trajet.AdresseArrivee,
|
||||||
|
trajet.Distance,
|
||||||
|
trajet.Commentaire,
|
||||||
|
trajet.Position,
|
||||||
|
trajet.VehiculeId,
|
||||||
|
trajet.PuissanceFiscale));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modifie un Trajet (Exigences 5.5, 5.6). Retourne 200 + <see cref="TrajetDto"/>. Un
|
||||||
|
/// avertissement de distance non calculable est porté par l'en-tête <c>X-Trajet-Warning</c>.
|
||||||
|
/// 404 si le Trajet est introuvable ; 503 si le recalcul de distance échoue (Exigence 5.8).
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{id:int}")]
|
||||||
|
[ProducesResponseType(typeof(TrajetDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
public async Task<ActionResult<TrajetDto>> Update(
|
||||||
|
int id,
|
||||||
|
[FromBody] UpdateTrajetDto dto,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resultat = await _trajetService.UpdateAsync(id, dto, cancellationToken);
|
||||||
|
|
||||||
|
if (resultat.Avertissement && !string.IsNullOrWhiteSpace(resultat.Message))
|
||||||
|
{
|
||||||
|
Response.Headers[WarningHeader] = resultat.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(resultat.Trajet);
|
||||||
|
}
|
||||||
|
catch (TrajetNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
catch (VehiculeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
catch (DistanceRecalculException ex)
|
||||||
|
{
|
||||||
|
// Échec du recalcul sur des coordonnées connues : modification annulée (rollback),
|
||||||
|
// données existantes préservées (Exigence 5.8). Mappé sur 503 conformément à la table
|
||||||
|
// d'erreurs de la conception (DistanceCalculationException → 503).
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Supprime un Trajet (Exigence 5.5). 204 si succès, 404 s'il est introuvable (5.6).</summary>
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _trajetService.DeleteAsync(id, cancellationToken);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (TrajetNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Réordonne manuellement les trajets d'une journée : met à jour uniquement leurs positions
|
||||||
|
/// sans altérer les autres champs (Exigence 11.9). 204 en cas de succès.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPatch("reordonner")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<IActionResult> Reordonner(
|
||||||
|
[FromBody] ReordonnerTrajetsDto dto,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _trajetService.ReordonnerAsync(dto, cancellationToken);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne les trajets d'une journée enrichis des coordonnées et géométries connues via le
|
||||||
|
/// Cache_Adresses (Exigences 8.7, 9.3). Le paramètre <paramref name="date"/> est attendu au
|
||||||
|
/// format ISO <c>yyyy-MM-dd</c> pour la sûreté du routage. Les coordonnées/géométries absentes
|
||||||
|
/// du cache restent à <c>null</c>.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("journee/{date}")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<TrajetCarteDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<TrajetCarteDto>>> GetJournee(
|
||||||
|
string date,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!DateOnly.TryParseExact(date, "yyyy-MM-dd", CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.None, out var jour))
|
||||||
|
{
|
||||||
|
return Problem(
|
||||||
|
detail: $"La date '{date}' est invalide. Format attendu : yyyy-MM-dd.",
|
||||||
|
statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
var trajets = await _trajetRepository.GetByDateAsync(jour, cancellationToken);
|
||||||
|
|
||||||
|
var resultats = new List<TrajetCarteDto>(trajets.Count);
|
||||||
|
foreach (var trajet in trajets)
|
||||||
|
{
|
||||||
|
var depart = await _adresseCacheService.ObtenirParAdresseAsync(
|
||||||
|
trajet.AdresseDepart, cancellationToken);
|
||||||
|
var arrivee = await _adresseCacheService.ObtenirParAdresseAsync(
|
||||||
|
trajet.AdresseArrivee, cancellationToken);
|
||||||
|
|
||||||
|
// Géométrie de l'itinéraire : portée par le Trajet lui-même (paire départ→arrivée),
|
||||||
|
// seule source correcte. Fallback sur le cache d'adresses pour d'éventuels trajets
|
||||||
|
// hérités dont la géométrie n'a pas encore été recalculée (rétrocompatibilité).
|
||||||
|
var geometrie = trajet.GeometrieJson ?? depart?.GeometrieJson ?? arrivee?.GeometrieJson;
|
||||||
|
|
||||||
|
resultats.Add(new TrajetCarteDto(
|
||||||
|
trajet.Id,
|
||||||
|
trajet.Date,
|
||||||
|
trajet.AdresseDepart,
|
||||||
|
trajet.AdresseArrivee,
|
||||||
|
trajet.Distance ?? 0m,
|
||||||
|
depart?.Latitude,
|
||||||
|
depart?.Longitude,
|
||||||
|
arrivee?.Latitude,
|
||||||
|
arrivee?.Longitude,
|
||||||
|
geometrie));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(resultats);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoints de gestion des véhicules : historique, véhicule courant, création et réaffectation.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/vehicules")]
|
||||||
|
public class VehiculesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IVehiculeService _vehiculeService;
|
||||||
|
|
||||||
|
public VehiculesController(IVehiculeService vehiculeService)
|
||||||
|
{
|
||||||
|
_vehiculeService = vehiculeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Historique complet des véhicules (trié par date d'effet décroissante).</summary>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<VehiculeDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<VehiculeDto>>> GetHistorique(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var vehicules = await _vehiculeService.GetHistoriqueAsync(cancellationToken);
|
||||||
|
return Ok(vehicules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Véhicule courant (date d'effet la plus récente ≤ aujourd'hui).</summary>
|
||||||
|
[HttpGet("courant")]
|
||||||
|
[ProducesResponseType(typeof(VehiculeDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<VehiculeDto>> GetCourant(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vehicule = await _vehiculeService.GetVehiculeCourantAsync(cancellationToken);
|
||||||
|
return Ok(vehicule);
|
||||||
|
}
|
||||||
|
catch (VehiculeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enregistre un véhicule. Retourne <see cref="VehiculeCreeDto"/> indiquant si une réaffectation
|
||||||
|
/// des trajets doit être proposée. Rejette une date d'effet future (400).
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(VehiculeCreeDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<VehiculeCreeDto>> Create(
|
||||||
|
[FromBody] CreateVehiculeDto dto,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resultat = await _vehiculeService.CreateAsync(dto, cancellationToken);
|
||||||
|
return CreatedAtAction(nameof(GetHistorique), null, resultat);
|
||||||
|
}
|
||||||
|
catch (DateEffetFutureException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Accepte ou refuse la réaffectation des trajets au véhicule indiqué.</summary>
|
||||||
|
[HttpPost("reaffectation")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> Reaffecter(
|
||||||
|
[FromBody] ReaffectationDto dto,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _vehiculeService.ReaffecterAsync(dto, cancellationToken);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (VehiculeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contexte EF Core de l'application. Déclare les <see cref="DbSet{TEntity}"/>, les contraintes CHECK,
|
||||||
|
/// les index et les index uniques conformément à la section « Schéma SQLite » de la conception.
|
||||||
|
/// </summary>
|
||||||
|
public class AppDbContext : DbContext
|
||||||
|
{
|
||||||
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<Trajet> Trajets => Set<Trajet>();
|
||||||
|
public DbSet<Vehicule> Vehicules => Set<Vehicule>();
|
||||||
|
public DbSet<Profil> Profil => Set<Profil>();
|
||||||
|
public DbSet<AdresseCache> AdresseCache => Set<AdresseCache>();
|
||||||
|
public DbSet<AdresseNommee> AdressesNommees => Set<AdresseNommee>();
|
||||||
|
public DbSet<TrancheBareme> TranchesBareme => Set<TrancheBareme>();
|
||||||
|
public DbSet<MontantRegle> MontantsRegles => Set<MontantRegle>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
// --- Trajets ---
|
||||||
|
modelBuilder.Entity<Trajet>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("Trajets", t =>
|
||||||
|
{
|
||||||
|
// Distance nulle autorisée (Exigence 2.4 : distance non calculable) ; sinon strictement positive.
|
||||||
|
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" IS NULL OR \"Distance\" > 0");
|
||||||
|
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
entity.HasIndex(t => t.Date, "IX_Trajets_Date");
|
||||||
|
entity.HasIndex(t => new { t.Date, t.Position }, "IX_Trajets_Date_Position");
|
||||||
|
entity.HasIndex(t => t.VehiculeId, "IX_Trajets_Vehicule");
|
||||||
|
|
||||||
|
entity.HasOne(t => t.Vehicule)
|
||||||
|
.WithMany(v => v.Trajets)
|
||||||
|
.HasForeignKey(t => t.VehiculeId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Vehicules ---
|
||||||
|
modelBuilder.Entity<Vehicule>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("Vehicules", t =>
|
||||||
|
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0"));
|
||||||
|
|
||||||
|
entity.HasIndex(v => v.DateEffet, "IX_Vehicules_DateEffet");
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Profil (singleton Id = 1) ---
|
||||||
|
modelBuilder.Entity<Profil>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("Profil");
|
||||||
|
entity.HasKey(p => p.Id);
|
||||||
|
entity.Property(p => p.Id).ValueGeneratedNever();
|
||||||
|
entity.HasIndex(p => p.Id, "UX_Profil_Singleton").IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- AdresseCache (Adresse unique) ---
|
||||||
|
modelBuilder.Entity<AdresseCache>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("AdresseCache");
|
||||||
|
entity.HasIndex(a => a.Adresse).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- AdressesNommees (unicité sur DenominationNormalisee) ---
|
||||||
|
modelBuilder.Entity<AdresseNommee>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("AdressesNommees");
|
||||||
|
entity.HasIndex(a => a.DenominationNormalisee, "UX_AdresseNommee_Denomination").IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- TranchesBareme ---
|
||||||
|
modelBuilder.Entity<TrancheBareme>(entity =>
|
||||||
|
{
|
||||||
|
// Convention [KmMin, KmMax[ : borne max EXCLUSIVE, ou NULL = illimité.
|
||||||
|
// La contrainte tolère KmMax NULL et impose sinon KmMin < KmMax.
|
||||||
|
// Convention [KmMin, KmMax[ : borne max EXCLUSIVE, ou NULL = illimité.
|
||||||
|
// Les décimaux sont stockés en TEXT par SQLite : on CAST en REAL pour comparer
|
||||||
|
// numériquement (sinon "5000" > "20000" en comparaison lexicographique de chaînes).
|
||||||
|
entity.ToTable("TranchesBareme", t =>
|
||||||
|
t.HasCheckConstraint(
|
||||||
|
"CK_TranchesBareme_Km",
|
||||||
|
"\"KmMax\" IS NULL OR CAST(\"KmMin\" AS REAL) < CAST(\"KmMax\" AS REAL)"));
|
||||||
|
|
||||||
|
entity.HasIndex(t => new { t.Annee, t.PuissanceFiscale }, "IX_TranchesBareme_Annee_PF");
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- MontantsRegles ---
|
||||||
|
modelBuilder.Entity<MontantRegle>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("MontantsRegles", t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
||||||
|
});
|
||||||
|
|
||||||
|
entity.HasIndex(m => new { m.Annee, m.Mois, m.VehiculeId }, "UX_MontantsRegles_Periode_Vehicule")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
entity.HasOne(m => m.Vehicule)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(m => m.VehiculeId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+325
@@ -0,0 +1,325 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260913083053_InitialCreate")]
|
||||||
|
partial class InitialCreate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseCache", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DerniereUtilisation")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Adresse")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdresseCache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseNommee", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DenominationNormalisee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("GeocodageReussi")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DenominationNormalisee" }, "UX_AdresseNommee_Denomination")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdressesNommees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Mois")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Montant")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VehiculeId");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "Mois", "VehiculeId" }, "UX_MontantsRegles_Periode_Vehicule")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MontantsRegles", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Profil", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDomicile")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DomicileGeocode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLatitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLongitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Matricule")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Nom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Prenom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Id" }, "UX_Profil_Singleton")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Profil", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseArrivee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDepart")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Commentaire")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Distance")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Position")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date" }, "IX_Trajets_Date");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date", "Position" }, "IX_Trajets_Date_Position");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "VehiculeId" }, "IX_Trajets_Vehicule");
|
||||||
|
|
||||||
|
b.ToTable("Trajets", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" > 0");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.TrancheBareme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Base")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Coefficient")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMax")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMin")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "PuissanceFiscale" }, "IX_TranchesBareme_Annee_PF");
|
||||||
|
|
||||||
|
b.ToTable("TranchesBareme", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_TranchesBareme_Km", "\"KmMin\" <= \"KmMax\"");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateEffet")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Plaque")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(15)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DateEffet" }, "IX_Vehicules_DateEffet");
|
||||||
|
|
||||||
|
b.ToTable("Vehicules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany("Trajets")
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Trajets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AdresseCache",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Adresse = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
|
||||||
|
Latitude = table.Column<double>(type: "REAL", nullable: false),
|
||||||
|
Longitude = table.Column<double>(type: "REAL", nullable: false),
|
||||||
|
GeometrieJson = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
DerniereUtilisation = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AdresseCache", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AdressesNommees",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Denomination = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||||
|
DenominationNormalisee = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||||
|
Adresse = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
|
||||||
|
Latitude = table.Column<double>(type: "REAL", nullable: true),
|
||||||
|
Longitude = table.Column<double>(type: "REAL", nullable: true),
|
||||||
|
GeocodageReussi = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AdressesNommees", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Profil",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Nom = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
Prenom = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
Matricule = table.Column<string>(type: "TEXT", maxLength: 8, nullable: true),
|
||||||
|
AdresseDomicile = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||||
|
DomicileLatitude = table.Column<double>(type: "REAL", nullable: true),
|
||||||
|
DomicileLongitude = table.Column<double>(type: "REAL", nullable: true),
|
||||||
|
DomicileGeocode = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Profil", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TranchesBareme",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Annee = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
PuissanceFiscale = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
KmMin = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||||
|
KmMax = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||||
|
Coefficient = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||||
|
Base = table.Column<decimal>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TranchesBareme", x => x.Id);
|
||||||
|
table.CheckConstraint("CK_TranchesBareme_Km", "\"KmMin\" <= \"KmMax\"");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Vehicules",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Plaque = table.Column<string>(type: "TEXT", maxLength: 15, nullable: false),
|
||||||
|
PuissanceFiscale = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
DateEffet = table.Column<DateOnly>(type: "TEXT", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Vehicules", x => x.Id);
|
||||||
|
table.CheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "MontantsRegles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Annee = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Mois = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
VehiculeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Montant = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_MontantsRegles", x => x.Id);
|
||||||
|
table.CheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
||||||
|
table.CheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MontantsRegles_Vehicules_VehiculeId",
|
||||||
|
column: x => x.VehiculeId,
|
||||||
|
principalTable: "Vehicules",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Trajets",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Date = table.Column<DateOnly>(type: "TEXT", nullable: false),
|
||||||
|
AdresseDepart = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
|
||||||
|
AdresseArrivee = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
|
||||||
|
Distance = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||||
|
Commentaire = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||||
|
Position = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||||
|
VehiculeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
PuissanceFiscale = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Trajets", x => x.Id);
|
||||||
|
table.CheckConstraint("CK_Trajets_Distance", "\"Distance\" > 0");
|
||||||
|
table.CheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Trajets_Vehicules_VehiculeId",
|
||||||
|
column: x => x.VehiculeId,
|
||||||
|
principalTable: "Vehicules",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AdresseCache_Adresse",
|
||||||
|
table: "AdresseCache",
|
||||||
|
column: "Adresse",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UX_AdresseNommee_Denomination",
|
||||||
|
table: "AdressesNommees",
|
||||||
|
column: "DenominationNormalisee",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MontantsRegles_VehiculeId",
|
||||||
|
table: "MontantsRegles",
|
||||||
|
column: "VehiculeId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UX_MontantsRegles_Periode_Vehicule",
|
||||||
|
table: "MontantsRegles",
|
||||||
|
columns: new[] { "Annee", "Mois", "VehiculeId" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UX_Profil_Singleton",
|
||||||
|
table: "Profil",
|
||||||
|
column: "Id",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Trajets_Date",
|
||||||
|
table: "Trajets",
|
||||||
|
column: "Date");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Trajets_Date_Position",
|
||||||
|
table: "Trajets",
|
||||||
|
columns: new[] { "Date", "Position" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Trajets_Vehicule",
|
||||||
|
table: "Trajets",
|
||||||
|
column: "VehiculeId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TranchesBareme_Annee_PF",
|
||||||
|
table: "TranchesBareme",
|
||||||
|
columns: new[] { "Annee", "PuissanceFiscale" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Vehicules_DateEffet",
|
||||||
|
table: "Vehicules",
|
||||||
|
column: "DateEffet");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AdresseCache");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AdressesNommees");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "MontantsRegles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Profil");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Trajets");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TranchesBareme");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Vehicules");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+325
@@ -0,0 +1,325 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260913085051_TrajetDistanceNullable")]
|
||||||
|
partial class TrajetDistanceNullable
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseCache", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DerniereUtilisation")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Adresse")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdresseCache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseNommee", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DenominationNormalisee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("GeocodageReussi")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DenominationNormalisee" }, "UX_AdresseNommee_Denomination")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdressesNommees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Mois")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Montant")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VehiculeId");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "Mois", "VehiculeId" }, "UX_MontantsRegles_Periode_Vehicule")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MontantsRegles", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Profil", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDomicile")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DomicileGeocode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLatitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLongitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Matricule")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Nom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Prenom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Id" }, "UX_Profil_Singleton")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Profil", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseArrivee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDepart")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Commentaire")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Distance")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Position")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date" }, "IX_Trajets_Date");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date", "Position" }, "IX_Trajets_Date_Position");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "VehiculeId" }, "IX_Trajets_Vehicule");
|
||||||
|
|
||||||
|
b.ToTable("Trajets", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" IS NULL OR \"Distance\" > 0");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.TrancheBareme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Base")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Coefficient")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMax")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMin")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "PuissanceFiscale" }, "IX_TranchesBareme_Annee_PF");
|
||||||
|
|
||||||
|
b.ToTable("TranchesBareme", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_TranchesBareme_Km", "\"KmMin\" <= \"KmMax\"");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateEffet")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Plaque")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(15)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DateEffet" }, "IX_Vehicules_DateEffet");
|
||||||
|
|
||||||
|
b.ToTable("Vehicules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany("Trajets")
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Trajets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class TrajetDistanceNullable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropCheckConstraint(
|
||||||
|
name: "CK_Trajets_Distance",
|
||||||
|
table: "Trajets");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<decimal>(
|
||||||
|
name: "Distance",
|
||||||
|
table: "Trajets",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(decimal),
|
||||||
|
oldType: "TEXT");
|
||||||
|
|
||||||
|
migrationBuilder.AddCheckConstraint(
|
||||||
|
name: "CK_Trajets_Distance",
|
||||||
|
table: "Trajets",
|
||||||
|
sql: "\"Distance\" IS NULL OR \"Distance\" > 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropCheckConstraint(
|
||||||
|
name: "CK_Trajets_Distance",
|
||||||
|
table: "Trajets");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<decimal>(
|
||||||
|
name: "Distance",
|
||||||
|
table: "Trajets",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m,
|
||||||
|
oldClrType: typeof(decimal),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddCheckConstraint(
|
||||||
|
name: "CK_Trajets_Distance",
|
||||||
|
table: "Trajets",
|
||||||
|
sql: "\"Distance\" > 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+328
@@ -0,0 +1,328 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260913224032_TrajetGeometrieJson")]
|
||||||
|
partial class TrajetGeometrieJson
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseCache", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DerniereUtilisation")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Adresse")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdresseCache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseNommee", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DenominationNormalisee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("GeocodageReussi")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DenominationNormalisee" }, "UX_AdresseNommee_Denomination")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdressesNommees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Mois")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Montant")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VehiculeId");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "Mois", "VehiculeId" }, "UX_MontantsRegles_Periode_Vehicule")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MontantsRegles", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Profil", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDomicile")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DomicileGeocode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLatitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLongitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Matricule")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Nom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Prenom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Id" }, "UX_Profil_Singleton")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Profil", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseArrivee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDepart")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Commentaire")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Distance")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Position")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date" }, "IX_Trajets_Date");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date", "Position" }, "IX_Trajets_Date_Position");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "VehiculeId" }, "IX_Trajets_Vehicule");
|
||||||
|
|
||||||
|
b.ToTable("Trajets", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" IS NULL OR \"Distance\" > 0");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.TrancheBareme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Base")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Coefficient")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMax")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMin")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "PuissanceFiscale" }, "IX_TranchesBareme_Annee_PF");
|
||||||
|
|
||||||
|
b.ToTable("TranchesBareme", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_TranchesBareme_Km", "\"KmMin\" <= \"KmMax\"");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateEffet")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Plaque")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(15)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DateEffet" }, "IX_Vehicules_DateEffet");
|
||||||
|
|
||||||
|
b.ToTable("Vehicules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany("Trajets")
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Trajets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class TrajetGeometrieJson : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "GeometrieJson",
|
||||||
|
table: "Trajets",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GeometrieJson",
|
||||||
|
table: "Trajets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+333
@@ -0,0 +1,333 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260913231424_VehiculeDenomination")]
|
||||||
|
partial class VehiculeDenomination
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseCache", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DerniereUtilisation")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Adresse")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdresseCache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseNommee", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DenominationNormalisee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("GeocodageReussi")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DenominationNormalisee" }, "UX_AdresseNommee_Denomination")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdressesNommees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Mois")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Montant")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VehiculeId");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "Mois", "VehiculeId" }, "UX_MontantsRegles_Periode_Vehicule")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MontantsRegles", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Profil", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDomicile")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DomicileGeocode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLatitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLongitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Matricule")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Nom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Prenom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Id" }, "UX_Profil_Singleton")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Profil", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseArrivee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDepart")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Commentaire")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Distance")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Position")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date" }, "IX_Trajets_Date");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date", "Position" }, "IX_Trajets_Date_Position");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "VehiculeId" }, "IX_Trajets_Vehicule");
|
||||||
|
|
||||||
|
b.ToTable("Trajets", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" IS NULL OR \"Distance\" > 0");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.TrancheBareme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Base")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Coefficient")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMax")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMin")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "PuissanceFiscale" }, "IX_TranchesBareme_Annee_PF");
|
||||||
|
|
||||||
|
b.ToTable("TranchesBareme", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_TranchesBareme_Km", "\"KmMin\" <= \"KmMax\"");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateEffet")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Plaque")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(15)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DateEffet" }, "IX_Vehicules_DateEffet");
|
||||||
|
|
||||||
|
b.ToTable("Vehicules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany("Trajets")
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Trajets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class VehiculeDenomination : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Denomination",
|
||||||
|
table: "Vehicules",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
// Véhicules déjà existants : dénomination par défaut = leur plaque, pour éviter
|
||||||
|
// une dénomination vide (le champ est désormais obligatoire côté saisie).
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"UPDATE \"Vehicules\" SET \"Denomination\" = \"Plaque\" " +
|
||||||
|
"WHERE \"Denomination\" IS NULL OR \"Denomination\" = '';");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Denomination",
|
||||||
|
table: "Vehicules");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+333
@@ -0,0 +1,333 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260913233718_TrancheBaremeKmMaxNullable")]
|
||||||
|
partial class TrancheBaremeKmMaxNullable
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseCache", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DerniereUtilisation")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Adresse")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdresseCache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseNommee", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DenominationNormalisee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("GeocodageReussi")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DenominationNormalisee" }, "UX_AdresseNommee_Denomination")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdressesNommees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Mois")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Montant")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VehiculeId");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "Mois", "VehiculeId" }, "UX_MontantsRegles_Periode_Vehicule")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MontantsRegles", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Profil", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDomicile")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DomicileGeocode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLatitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLongitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Matricule")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Nom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Prenom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Id" }, "UX_Profil_Singleton")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Profil", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseArrivee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDepart")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Commentaire")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Distance")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Position")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date" }, "IX_Trajets_Date");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date", "Position" }, "IX_Trajets_Date_Position");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "VehiculeId" }, "IX_Trajets_Vehicule");
|
||||||
|
|
||||||
|
b.ToTable("Trajets", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" IS NULL OR \"Distance\" > 0");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.TrancheBareme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Base")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Coefficient")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("KmMax")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMin")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "PuissanceFiscale" }, "IX_TranchesBareme_Annee_PF");
|
||||||
|
|
||||||
|
b.ToTable("TranchesBareme", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_TranchesBareme_Km", "\"KmMin\" <= \"KmMax\"");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateEffet")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Plaque")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(15)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DateEffet" }, "IX_Vehicules_DateEffet");
|
||||||
|
|
||||||
|
b.ToTable("Vehicules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany("Trajets")
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Trajets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class TrancheBaremeKmMaxNullable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<decimal>(
|
||||||
|
name: "KmMax",
|
||||||
|
table: "TranchesBareme",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(decimal),
|
||||||
|
oldType: "TEXT");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<decimal>(
|
||||||
|
name: "KmMax",
|
||||||
|
table: "TranchesBareme",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m,
|
||||||
|
oldClrType: typeof(decimal),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+333
@@ -0,0 +1,333 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260913234022_TrancheBaremeKmCheckExclusif")]
|
||||||
|
partial class TrancheBaremeKmCheckExclusif
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseCache", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DerniereUtilisation")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Adresse")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdresseCache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseNommee", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DenominationNormalisee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("GeocodageReussi")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DenominationNormalisee" }, "UX_AdresseNommee_Denomination")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdressesNommees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Mois")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Montant")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VehiculeId");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "Mois", "VehiculeId" }, "UX_MontantsRegles_Periode_Vehicule")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MontantsRegles", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Profil", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDomicile")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DomicileGeocode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLatitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLongitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Matricule")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Nom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Prenom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Id" }, "UX_Profil_Singleton")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Profil", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseArrivee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDepart")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Commentaire")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Distance")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Position")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date" }, "IX_Trajets_Date");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date", "Position" }, "IX_Trajets_Date_Position");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "VehiculeId" }, "IX_Trajets_Vehicule");
|
||||||
|
|
||||||
|
b.ToTable("Trajets", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" IS NULL OR \"Distance\" > 0");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.TrancheBareme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Base")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Coefficient")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("KmMax")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMin")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "PuissanceFiscale" }, "IX_TranchesBareme_Annee_PF");
|
||||||
|
|
||||||
|
b.ToTable("TranchesBareme", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_TranchesBareme_Km", "\"KmMax\" IS NULL OR \"KmMin\" < \"KmMax\"");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateEffet")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Plaque")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(15)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DateEffet" }, "IX_Vehicules_DateEffet");
|
||||||
|
|
||||||
|
b.ToTable("Vehicules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany("Trajets")
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Trajets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class TrancheBaremeKmCheckExclusif : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropCheckConstraint(
|
||||||
|
name: "CK_TranchesBareme_Km",
|
||||||
|
table: "TranchesBareme");
|
||||||
|
|
||||||
|
migrationBuilder.AddCheckConstraint(
|
||||||
|
name: "CK_TranchesBareme_Km",
|
||||||
|
table: "TranchesBareme",
|
||||||
|
sql: "\"KmMax\" IS NULL OR \"KmMin\" < \"KmMax\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropCheckConstraint(
|
||||||
|
name: "CK_TranchesBareme_Km",
|
||||||
|
table: "TranchesBareme");
|
||||||
|
|
||||||
|
migrationBuilder.AddCheckConstraint(
|
||||||
|
name: "CK_TranchesBareme_Km",
|
||||||
|
table: "TranchesBareme",
|
||||||
|
sql: "\"KmMin\" <= \"KmMax\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
backend/TrajetKmTracker.Api/Data/Migrations/20260913234153_TrancheBaremeKmCheckNumerique.Designer.cs
Generated
+333
@@ -0,0 +1,333 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260913234153_TrancheBaremeKmCheckNumerique")]
|
||||||
|
partial class TrancheBaremeKmCheckNumerique
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseCache", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DerniereUtilisation")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Adresse")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdresseCache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseNommee", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DenominationNormalisee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("GeocodageReussi")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DenominationNormalisee" }, "UX_AdresseNommee_Denomination")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdressesNommees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Mois")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Montant")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VehiculeId");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "Mois", "VehiculeId" }, "UX_MontantsRegles_Periode_Vehicule")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MontantsRegles", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Profil", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDomicile")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DomicileGeocode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLatitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLongitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Matricule")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Nom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Prenom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Id" }, "UX_Profil_Singleton")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Profil", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseArrivee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDepart")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Commentaire")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Distance")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Position")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date" }, "IX_Trajets_Date");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date", "Position" }, "IX_Trajets_Date_Position");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "VehiculeId" }, "IX_Trajets_Vehicule");
|
||||||
|
|
||||||
|
b.ToTable("Trajets", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" IS NULL OR \"Distance\" > 0");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.TrancheBareme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Base")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Coefficient")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("KmMax")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMin")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "PuissanceFiscale" }, "IX_TranchesBareme_Annee_PF");
|
||||||
|
|
||||||
|
b.ToTable("TranchesBareme", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_TranchesBareme_Km", "\"KmMax\" IS NULL OR CAST(\"KmMin\" AS REAL) < CAST(\"KmMax\" AS REAL)");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateEffet")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Plaque")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(15)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DateEffet" }, "IX_Vehicules_DateEffet");
|
||||||
|
|
||||||
|
b.ToTable("Vehicules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany("Trajets")
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Trajets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class TrancheBaremeKmCheckNumerique : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropCheckConstraint(
|
||||||
|
name: "CK_TranchesBareme_Km",
|
||||||
|
table: "TranchesBareme");
|
||||||
|
|
||||||
|
migrationBuilder.AddCheckConstraint(
|
||||||
|
name: "CK_TranchesBareme_Km",
|
||||||
|
table: "TranchesBareme",
|
||||||
|
sql: "\"KmMax\" IS NULL OR CAST(\"KmMin\" AS REAL) < CAST(\"KmMax\" AS REAL)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropCheckConstraint(
|
||||||
|
name: "CK_TranchesBareme_Km",
|
||||||
|
table: "TranchesBareme");
|
||||||
|
|
||||||
|
migrationBuilder.AddCheckConstraint(
|
||||||
|
name: "CK_TranchesBareme_Km",
|
||||||
|
table: "TranchesBareme",
|
||||||
|
sql: "\"KmMax\" IS NULL OR \"KmMin\" < \"KmMax\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseCache", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DerniereUtilisation")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Adresse")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdresseCache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.AdresseNommee", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Adresse")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DenominationNormalisee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("GeocodageReussi")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("Latitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("Longitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DenominationNormalisee" }, "UX_AdresseNommee_Denomination")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AdressesNommees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Mois")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Montant")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VehiculeId");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "Mois", "VehiculeId" }, "UX_MontantsRegles_Periode_Vehicule")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MontantsRegles", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Profil", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDomicile")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DomicileGeocode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLatitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double?>("DomicileLongitude")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Matricule")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Nom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Prenom")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Id" }, "UX_Profil_Singleton")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Profil", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseArrivee")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AdresseDepart")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Commentaire")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Distance")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GeometrieJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Position")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VehiculeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date" }, "IX_Trajets_Date");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Date", "Position" }, "IX_Trajets_Date_Position");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "VehiculeId" }, "IX_Trajets_Vehicule");
|
||||||
|
|
||||||
|
b.ToTable("Trajets", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" IS NULL OR \"Distance\" > 0");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.TrancheBareme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Annee")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<decimal>("Base")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Coefficient")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal?>("KmMax")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("KmMin")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Annee", "PuissanceFiscale" }, "IX_TranchesBareme_Annee_PF");
|
||||||
|
|
||||||
|
b.ToTable("TranchesBareme", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_TranchesBareme_Km", "\"KmMax\" IS NULL OR CAST(\"KmMin\" AS REAL) < CAST(\"KmMax\" AS REAL)");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateEffet")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Denomination")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Plaque")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(15)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PuissanceFiscale")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "DateEffet" }, "IX_Vehicules_DateEffet");
|
||||||
|
|
||||||
|
b.ToTable("Vehicules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.MontantRegle", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Trajet", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TrajetKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
|
||||||
|
.WithMany("Trajets")
|
||||||
|
.HasForeignKey("VehiculeId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Vehicule");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrajetKmTracker.Api.Models.Entities.Vehicule", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Trajets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
// Résultat de recherche BAN (proxied via backend si nécessaire)
|
||||||
|
public record BANAdresseDto(
|
||||||
|
string Label,
|
||||||
|
double Latitude,
|
||||||
|
double Longitude,
|
||||||
|
string Postcode,
|
||||||
|
string City
|
||||||
|
);
|
||||||
|
|
||||||
|
// Suggestion autocomplétion
|
||||||
|
public record AdresseCacheDto(
|
||||||
|
string Adresse,
|
||||||
|
double Latitude,
|
||||||
|
double Longitude
|
||||||
|
);
|
||||||
|
|
||||||
|
// Entrée complète du Cache_Adresses (lookup exact par la création de Trajet)
|
||||||
|
public record AdresseCacheEntreeDto(
|
||||||
|
string Adresse,
|
||||||
|
double Latitude,
|
||||||
|
double Longitude,
|
||||||
|
string? GeometrieJson
|
||||||
|
);
|
||||||
|
|
||||||
|
// Carnet d'adresses — Adresse_Nommee retournée par le backend
|
||||||
|
public record AdresseNommeeDto(
|
||||||
|
int Id,
|
||||||
|
string Denomination,
|
||||||
|
string Adresse,
|
||||||
|
double? Latitude, // null si le géocodage a échoué
|
||||||
|
double? Longitude // null si le géocodage a échoué
|
||||||
|
);
|
||||||
|
|
||||||
|
// Carnet d'adresses — création / mise à jour d'une Adresse_Nommee
|
||||||
|
public record SaveAdresseNommeeDto(
|
||||||
|
string Denomination, // 1 à 100 car., unique après normalisation
|
||||||
|
string Adresse, // 1 à 255 car.
|
||||||
|
double? Latitude, // fournies par le frontend si sélection BAN
|
||||||
|
double? Longitude
|
||||||
|
);
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
// Barème
|
||||||
|
public record TrancheBaremeDto(
|
||||||
|
int Id,
|
||||||
|
int Annee,
|
||||||
|
int PuissanceFiscale,
|
||||||
|
decimal KmMin,
|
||||||
|
decimal? KmMax, // null = illimité (borne max exclusive : [KmMin, KmMax[)
|
||||||
|
decimal Coefficient,
|
||||||
|
decimal Base
|
||||||
|
);
|
||||||
|
|
||||||
|
public record SaveTrancheBaremeDto(
|
||||||
|
int Annee,
|
||||||
|
int PuissanceFiscale,
|
||||||
|
decimal KmMin,
|
||||||
|
decimal? KmMax, // null = illimité (borne max exclusive : [KmMin, KmMax[)
|
||||||
|
decimal Coefficient,
|
||||||
|
decimal Base
|
||||||
|
);
|
||||||
|
|
||||||
|
// Montant réglé
|
||||||
|
public record MontantRegleDto(
|
||||||
|
int Annee,
|
||||||
|
int Mois,
|
||||||
|
int VehiculeId,
|
||||||
|
decimal Montant
|
||||||
|
);
|
||||||
|
|
||||||
|
// Indication de versement mensuel (informatif) affichée à la saisie
|
||||||
|
public record MontantPrevisionnelDto(
|
||||||
|
int Annee,
|
||||||
|
int Mois,
|
||||||
|
int VehiculeId,
|
||||||
|
decimal TotalAnnuelPartielKm,
|
||||||
|
decimal MontantBaremePlein, // barème plein sur le total partiel
|
||||||
|
decimal MontantPrevisionnel // = 80% (indicatif uniquement)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ligne mensuelle de la synthèse annuelle (aligne sur la synthèse réelle)
|
||||||
|
public record LigneSyntheseMoisDto(
|
||||||
|
int Mois,
|
||||||
|
decimal KmMois, // kilomètres parcourus dans le mois
|
||||||
|
decimal FraisPaye, // Montant_Regle du mois (saisi)
|
||||||
|
decimal FraisReelMensuel // km_du_mois × coefficient de la tranche annuelle
|
||||||
|
);
|
||||||
|
|
||||||
|
// Synthèse annuelle de régularisation pour un véhicule (page 1 du rapport)
|
||||||
|
public record RegularisationAnnuelleDto(
|
||||||
|
int Annee,
|
||||||
|
int VehiculeId,
|
||||||
|
string Plaque,
|
||||||
|
int PuissanceFiscale,
|
||||||
|
decimal CoefficientTranche, // coefficient de la tranche annuelle applicable
|
||||||
|
decimal BaseTranche, // base de la tranche annuelle applicable
|
||||||
|
string TrancheLibelle, // ex. "5 001 à 20 000 km — D2"
|
||||||
|
IReadOnlyList<LigneSyntheseMoisDto> Mois, // 12 lignes
|
||||||
|
decimal TotalKmAnnuel,
|
||||||
|
decimal TotalPaye, // somme des Montant_Regle
|
||||||
|
decimal MontantReel, // barème plein : total_km × coef + base
|
||||||
|
decimal Regularisation // MontantReel − TotalPaye
|
||||||
|
);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
/// <summary>Résultat de la copie du dernier barème vers une année cible.</summary>
|
||||||
|
/// <param name="TranchesCopiees">Nombre de tranches copiées (0 si aucun barème antérieur).</param>
|
||||||
|
public record CopieBaremeResultDto(int TranchesCopiees);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
// Profil
|
||||||
|
public record ProfilDto(
|
||||||
|
string? Nom,
|
||||||
|
string? Prenom,
|
||||||
|
string? Matricule,
|
||||||
|
string? AdresseDomicile,
|
||||||
|
bool DomicileGeocode
|
||||||
|
);
|
||||||
|
|
||||||
|
public record SaveProfilDto(
|
||||||
|
string? Nom,
|
||||||
|
string? Prenom,
|
||||||
|
string? Matricule, // max 8 car.
|
||||||
|
string AdresseDomicile
|
||||||
|
);
|
||||||
|
|
||||||
|
// État de configuration du profil (onboarding première utilisation)
|
||||||
|
public record EtatProfilDto(
|
||||||
|
bool EstConfigure // true ssi Profil.Nom et Profil.Prenom non vides ET au moins un Vehicule existe
|
||||||
|
);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
/// <summary>Résultat du recalcul en masse des distances des trajets.</summary>
|
||||||
|
/// <param name="TrajetsRecalcules">Nombre de trajets dont la distance a été recalculée.</param>
|
||||||
|
public record RecalculDistancesResultDto(int TrajetsRecalcules);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
// Résumé mensuel
|
||||||
|
public record ResumeMensuelDto(
|
||||||
|
int Annee,
|
||||||
|
int Mois,
|
||||||
|
IReadOnlyList<TrajetDto> Trajets,
|
||||||
|
decimal TotalKm // somme distances, 2 décimales
|
||||||
|
);
|
||||||
|
|
||||||
|
// Résumé annuel
|
||||||
|
public record ResumeAnnuelDto(
|
||||||
|
int Annee,
|
||||||
|
IReadOnlyList<ResumeMensuelDto> Mois, // toujours 12 éléments
|
||||||
|
decimal TotalKm
|
||||||
|
);
|
||||||
|
|
||||||
|
// Totaux affichés dans le Panneau_Totaux
|
||||||
|
public record TotauxDto(
|
||||||
|
DateOnly Date,
|
||||||
|
int VehiculeId, // véhicule sur lequel portent les totaux
|
||||||
|
decimal TotalJour,
|
||||||
|
decimal TotalMois,
|
||||||
|
decimal TotalAnnuelPartiel
|
||||||
|
);
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
// Création — les coordonnées sont transmises par le Frontend (issues de la sélection BAN)
|
||||||
|
public record CreateTrajetDto(
|
||||||
|
string Date, // JJ/MM/AAAA en entrée
|
||||||
|
string AdresseDepart,
|
||||||
|
double? DepartLatitude, // null si adresse saisie manuellement sans BAN
|
||||||
|
double? DepartLongitude,
|
||||||
|
string AdresseArrivee,
|
||||||
|
double? ArriveeLatitude, // null si adresse saisie manuellement sans BAN
|
||||||
|
double? ArriveeLongitude,
|
||||||
|
string? Commentaire, // optionnel, max 500 car.
|
||||||
|
int? VehiculeId // optionnel ; null → véhicule courant à la date du trajet
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mise à jour
|
||||||
|
public record UpdateTrajetDto(
|
||||||
|
string Date,
|
||||||
|
string AdresseDepart,
|
||||||
|
double? DepartLatitude,
|
||||||
|
double? DepartLongitude,
|
||||||
|
string AdresseArrivee,
|
||||||
|
double? ArriveeLatitude,
|
||||||
|
double? ArriveeLongitude,
|
||||||
|
string? Commentaire,
|
||||||
|
int? VehiculeId // optionnel ; null → véhicule courant à la date du trajet
|
||||||
|
);
|
||||||
|
|
||||||
|
// Réponse trajet
|
||||||
|
public record TrajetDto(
|
||||||
|
int Id,
|
||||||
|
DateOnly Date,
|
||||||
|
string AdresseDepart,
|
||||||
|
string AdresseArrivee,
|
||||||
|
decimal? Distance, // km, 2 décimales ; null si la distance n'a pas pu être calculée (Exigence 2.4)
|
||||||
|
string? Commentaire,
|
||||||
|
decimal Position, // rang dans la journée
|
||||||
|
int VehiculeId,
|
||||||
|
int PuissanceFiscale
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trajet enrichi pour la carte
|
||||||
|
public record TrajetCarteDto(
|
||||||
|
int Id,
|
||||||
|
DateOnly Date,
|
||||||
|
string AdresseDepart,
|
||||||
|
string AdresseArrivee,
|
||||||
|
decimal Distance,
|
||||||
|
double? DepartLatitude,
|
||||||
|
double? DepartLongitude,
|
||||||
|
double? ArriveeLatitude,
|
||||||
|
double? ArriveeLongitude,
|
||||||
|
string? GeometrieJson // GeoJSON LineString ou null
|
||||||
|
);
|
||||||
|
|
||||||
|
// Réordonnancement manuel des trajets d'une journée
|
||||||
|
public record ReordonnerTrajetsDto(
|
||||||
|
IReadOnlyList<TrajetPositionDto> Trajets
|
||||||
|
);
|
||||||
|
|
||||||
|
public record TrajetPositionDto(
|
||||||
|
int Id,
|
||||||
|
decimal Position
|
||||||
|
);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
// Véhicules
|
||||||
|
public record VehiculeDto(
|
||||||
|
int Id,
|
||||||
|
string Denomination,
|
||||||
|
string Plaque,
|
||||||
|
int PuissanceFiscale,
|
||||||
|
DateOnly DateEffet
|
||||||
|
);
|
||||||
|
|
||||||
|
public record CreateVehiculeDto(
|
||||||
|
string Denomination,
|
||||||
|
string Plaque,
|
||||||
|
int PuissanceFiscale,
|
||||||
|
string DateEffet // JJ/MM/AAAA, <= date du jour
|
||||||
|
);
|
||||||
|
|
||||||
|
// Réponse à la création d'un véhicule : indique s'il faut proposer une réaffectation
|
||||||
|
public record VehiculeCreeDto(
|
||||||
|
VehiculeDto Vehicule,
|
||||||
|
bool ReaffectationProposee, // true si des trajets >= DateEffet ont une autre puissance fiscale
|
||||||
|
int NombreTrajetsConcernes
|
||||||
|
);
|
||||||
|
|
||||||
|
// Requête de réaffectation
|
||||||
|
public record ReaffectationDto(
|
||||||
|
int VehiculeId, // nouveau véhicule
|
||||||
|
bool Accepter // true = réaffecter les trajets >= DateEffet ; false = ne rien changer
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
/// <summary>Cache des adresses géocodées via ORS.</summary>
|
||||||
|
public class AdresseCache
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string Adresse { get; set; } = string.Empty; // texte normalisé
|
||||||
|
|
||||||
|
public double Latitude { get; set; } // précision 6 décimales
|
||||||
|
public double Longitude { get; set; } // précision 6 décimales
|
||||||
|
|
||||||
|
/// <summary>Géométrie GeoJSON de l'itinéraire (null si indisponible).</summary>
|
||||||
|
public string? GeometrieJson { get; set; }
|
||||||
|
|
||||||
|
public DateTime DerniereUtilisation { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adresse enregistrée dans le Carnet_Adresses, identifiée par une Dénomination unique
|
||||||
|
/// et réutilisable par sa Dénomination lors de la saisie/insertion d'un Trajet.
|
||||||
|
/// </summary>
|
||||||
|
public class AdresseNommee
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Libellé libre et unique identifiant l'adresse (1 à 100 caractères).</summary>
|
||||||
|
[Required, MaxLength(100)]
|
||||||
|
public string Denomination { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forme normalisée de la Dénomination (trim + minuscules + sans accents) servant de clé
|
||||||
|
/// d'unicité via l'index unique UX_AdresseNommee_Denomination. Calculée par le backend.
|
||||||
|
/// </summary>
|
||||||
|
[Required, MaxLength(100)]
|
||||||
|
public string DenominationNormalisee { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Texte d'adresse résolu via l'autocomplétion BAN (1 à 255 caractères).</summary>
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string Adresse { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Latitude (précision 6 décimales) ; null si le géocodage a échoué.</summary>
|
||||||
|
public double? Latitude { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Longitude (précision 6 décimales) ; null si le géocodage a échoué.</summary>
|
||||||
|
public double? Longitude { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Vrai si les coordonnées ont été obtenues (cache ou BAN) ; faux sinon.</summary>
|
||||||
|
public bool GeocodageReussi { get; set; } = false;
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
/// <summary>Montant réglé (remboursement reçu) pour un mois, une année et un véhicule.</summary>
|
||||||
|
public class MontantRegle
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int Annee { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int Mois { get; set; } // 1–12
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int VehiculeId { get; set; }
|
||||||
|
public Vehicule? Vehicule { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Montant réglé en euros (>= 0), deux décimales.</summary>
|
||||||
|
[Required]
|
||||||
|
public decimal Montant { get; set; }
|
||||||
|
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
/// <summary>Profil utilisateur (singleton — une seule ligne).</summary>
|
||||||
|
public class Profil
|
||||||
|
{
|
||||||
|
public int Id { get; set; } = 1; // toujours 1
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? Nom { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? Prenom { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Matricule (8 caractères maximum).</summary>
|
||||||
|
[MaxLength(8)]
|
||||||
|
public string? Matricule { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string? AdresseDomicile { get; set; }
|
||||||
|
|
||||||
|
public double? DomicileLatitude { get; set; }
|
||||||
|
public double? DomicileLongitude { get; set; }
|
||||||
|
public bool DomicileGeocode { get; set; } = false;
|
||||||
|
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
/// <summary>Trajet kilométrique saisi par l'utilisateur.</summary>
|
||||||
|
public class Trajet
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateOnly Date { get; set; }
|
||||||
|
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string AdresseDepart { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string AdresseArrivee { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Distance en kilomètres, arrondie à 2 décimales, strictement positive lorsqu'elle est
|
||||||
|
/// connue. <c>null</c> lorsque la distance n'a pas pu être calculée (aucune coordonnée
|
||||||
|
/// résoluble via le Cache_Adresses / la BAN) : le Trajet est alors persisté avec une
|
||||||
|
/// Distance nulle et un avertissement (Exigence 2.4). La modification ultérieure peut
|
||||||
|
/// recalculer la Distance si de nouvelles coordonnées deviennent disponibles (Exigence 5.10).
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Distance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Commentaire optionnel (0–500 caractères).</summary>
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Commentaire { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Géométrie GeoJSON (LineString) de l'itinéraire routier départ→arrivée, telle que
|
||||||
|
/// renvoyée par OpenRouteService, pour le tracé exact sur la carte. <c>null</c> lorsque
|
||||||
|
/// l'itinéraire routier n'a pas pu être obtenu (fallback vol d'oiseau) : la carte trace
|
||||||
|
/// alors une ligne droite. La géométrie dépend de la PAIRE départ→arrivée, c'est pourquoi
|
||||||
|
/// elle est portée par le Trajet et non par le Cache_Adresses (indexé par adresse seule).
|
||||||
|
/// </summary>
|
||||||
|
public string? GeometrieJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rang du trajet dans la liste de la journée. Valeur décimale (ex. 1.0, 1.5, 2.0)
|
||||||
|
/// permettant l'insertion sans réordonnancement systématique.
|
||||||
|
/// Trié par ordre croissant pour l'affichage.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public decimal Position { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Référence au véhicule associé au trajet (véhicule courant à la date de création par défaut).</summary>
|
||||||
|
[Required]
|
||||||
|
public int VehiculeId { get; set; }
|
||||||
|
public Vehicule? Vehicule { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Puissance fiscale (CV) du véhicule au moment de l'association ; stockée pour l'historique.</summary>
|
||||||
|
[Required]
|
||||||
|
public int PuissanceFiscale { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tranche du barème kilométrique URSSAF pour une année et une puissance fiscale.
|
||||||
|
/// Trois tranches par (année, puissance fiscale) : ≤5000 km, 5001–20000 km, >20000 km.
|
||||||
|
/// </summary>
|
||||||
|
public class TrancheBareme
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int Annee { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int PuissanceFiscale { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Borne minimale de kilométrage annuel (incluse).</summary>
|
||||||
|
[Required]
|
||||||
|
public decimal KmMin { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Borne maximale de kilométrage annuel, EXCLUSIVE : la tranche couvre [KmMin, KmMax[.
|
||||||
|
/// <c>null</c> signifie « illimité » (dernière tranche, sans borne supérieure).
|
||||||
|
/// </summary>
|
||||||
|
public decimal? KmMax { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Coefficient en euros par kilomètre.</summary>
|
||||||
|
[Required]
|
||||||
|
public decimal Coefficient { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Base fixe en euros ajoutée au montant (nulle pour les tranches 1 et 3). Formule : km × Coefficient + Base.</summary>
|
||||||
|
public decimal Base { get; set; } = 0m;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
/// <summary>Véhicule de l'utilisateur avec date d'effet. Historique conservé.</summary>
|
||||||
|
public class Vehicule
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Dénomination libre du véhicule (ex. « Voiture personnelle »), obligatoire (1–100 caractères).</summary>
|
||||||
|
[Required, MaxLength(100)]
|
||||||
|
public string Denomination { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Plaque d'immatriculation (1–15 caractères).</summary>
|
||||||
|
[Required, MaxLength(15)]
|
||||||
|
public string Plaque { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Puissance fiscale en chevaux fiscaux (entier positif).</summary>
|
||||||
|
[Required]
|
||||||
|
public int PuissanceFiscale { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Date à partir de laquelle ce véhicule devient le véhicule courant (<= date du jour).</summary>
|
||||||
|
[Required]
|
||||||
|
public DateOnly DateEffet { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public ICollection<Trajet> Trajets { get; set; } = new List<Trajet>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.AspNetCore;
|
||||||
|
using Microsoft.AspNetCore.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
using TrajetKmTracker.Api.Validators;
|
||||||
|
|
||||||
|
using TrajetKmTracker.Api.Configuration;
|
||||||
|
using TrajetKmTracker.Api.Repositories;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
// Licence QuestPDF (usage communautaire, gratuit) — requise avant toute génération de PDF.
|
||||||
|
QuestPDF.Settings.License = QuestPDF.Infrastructure.LicenseType.Community;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// --- Options de configuration liées depuis appsettings.json ---
|
||||||
|
builder.Services.Configure<BANOptions>(
|
||||||
|
builder.Configuration.GetSection(BANOptions.SectionName));
|
||||||
|
builder.Services.Configure<ORSOptions>(
|
||||||
|
builder.Configuration.GetSection(ORSOptions.SectionName));
|
||||||
|
|
||||||
|
// --- Client typé BAN (Base Adresses Nationale) ---
|
||||||
|
// BaseAddress et Timeout proviennent de BANOptions (appsettings.json).
|
||||||
|
var banOptions = builder.Configuration
|
||||||
|
.GetSection(BANOptions.SectionName)
|
||||||
|
.Get<BANOptions>() ?? new BANOptions();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<IBANClient, BANClient>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(banOptions.BaseUrl);
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(banOptions.TimeoutSeconds);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Client typé ORS (OpenRouteService) ---
|
||||||
|
// BaseAddress, Timeout et en-tête de clé d'API proviennent de ORSOptions (appsettings.json).
|
||||||
|
var orsOptions = builder.Configuration
|
||||||
|
.GetSection(ORSOptions.SectionName)
|
||||||
|
.Get<ORSOptions>() ?? new ORSOptions();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<IORSClient, ORSClient>(client =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(orsOptions.BaseUrl))
|
||||||
|
{
|
||||||
|
// Le slash final garantit la résolution correcte des chemins relatifs.
|
||||||
|
var baseUrl = orsOptions.BaseUrl.EndsWith('/')
|
||||||
|
? orsOptions.BaseUrl
|
||||||
|
: orsOptions.BaseUrl + "/";
|
||||||
|
client.BaseAddress = new Uri(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(orsOptions.TimeoutSeconds);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(orsOptions.ApiKey))
|
||||||
|
{
|
||||||
|
// ORS attend la clé brute dans l'en-tête Authorization (sans schéma « Bearer »).
|
||||||
|
// Les clés ORS (JWT) contiennent des caractères refusés par la validation stricte
|
||||||
|
// de DefaultRequestHeaders.Add ; on utilise TryAddWithoutValidation pour l'insérer
|
||||||
|
// telle quelle et éviter une FormatException à la création du client.
|
||||||
|
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", orsOptions.ApiKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Calculateur de distance (orchestration BAN + ORS + fallback Haversine) ---
|
||||||
|
builder.Services.AddScoped<IDistanceCalculator, DistanceCalculatorService>();
|
||||||
|
|
||||||
|
// --- Cache des adresses géocodées ---
|
||||||
|
builder.Services.AddScoped<IAdresseCacheRepository, AdresseCacheRepository>();
|
||||||
|
builder.Services.AddScoped<IAdresseCacheService, AdresseCacheService>();
|
||||||
|
|
||||||
|
// --- Services de base ---
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
// --- Health checks (endpoint GET /health, utilisé par Docker Compose) ---
|
||||||
|
builder.Services.AddHealthChecks();
|
||||||
|
|
||||||
|
// --- Validation des requêtes (FluentValidation) ---
|
||||||
|
// Enregistre tous les validateurs de l'assembly et active la validation automatique côté MVC.
|
||||||
|
builder.Services.AddValidatorsFromAssemblyContaining<CreateTrajetDtoValidator>();
|
||||||
|
builder.Services.AddFluentValidationAutoValidation();
|
||||||
|
|
||||||
|
// --- Base de données (EF Core + SQLite) ---
|
||||||
|
// La chaîne de connexion provient de la configuration ("ConnectionStrings:Default").
|
||||||
|
var connectionString = builder.Configuration.GetConnectionString("Default")
|
||||||
|
?? "Data Source=trajets.db";
|
||||||
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||||
|
options.UseSqlite(connectionString));
|
||||||
|
|
||||||
|
// --- Repositories & services métier ---
|
||||||
|
builder.Services.AddScoped<TrajetKmTracker.Api.Repositories.IVehiculeRepository,
|
||||||
|
TrajetKmTracker.Api.Repositories.VehiculeRepository>();
|
||||||
|
builder.Services.AddScoped<IVehiculeService, VehiculeService>();
|
||||||
|
builder.Services.AddScoped<IProfilService, ProfilService>();
|
||||||
|
builder.Services.AddScoped<IAdresseNommeeService, AdresseNommeeService>();
|
||||||
|
builder.Services.AddScoped<IBaremeService, BaremeService>();
|
||||||
|
builder.Services.AddScoped<IMontantRegleService, MontantRegleService>();
|
||||||
|
builder.Services.AddScoped<IRemboursementService, RemboursementService>();
|
||||||
|
builder.Services.AddScoped<ITotauxService, TotauxService>();
|
||||||
|
builder.Services.AddScoped<ITrajetRepository, TrajetRepository>();
|
||||||
|
builder.Services.AddScoped<ITrajetService, TrajetService>();
|
||||||
|
builder.Services.AddScoped<IResumeService, ResumeService>();
|
||||||
|
builder.Services.AddScoped<IPdfService, PdfService>();
|
||||||
|
|
||||||
|
// Exploration des endpoints + Swagger / OpenAPI
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = "SchotzIAD KM Tracker API",
|
||||||
|
Version = "v1",
|
||||||
|
Description = "API de saisie, calcul et consultation des trajets kilométriques."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ProblemDetails : réponses d'erreur normalisées (RFC 7807)
|
||||||
|
builder.Services.AddProblemDetails();
|
||||||
|
|
||||||
|
// CORS pour le frontend Angular.
|
||||||
|
// Les origines autorisées sont configurables via appsettings ("Cors:AllowedOrigins").
|
||||||
|
const string CorsPolicyName = "AngularFrontend";
|
||||||
|
var allowedOrigins = builder.Configuration
|
||||||
|
.GetSection("Cors:AllowedOrigins")
|
||||||
|
.Get<string[]>() ?? new[] { "http://localhost:4200" };
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy(CorsPolicyName, policy =>
|
||||||
|
{
|
||||||
|
policy.WithOrigins(allowedOrigins)
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// --- Pipeline HTTP ---
|
||||||
|
|
||||||
|
// Gestion centralisée des exceptions -> ProblemDetails
|
||||||
|
app.UseExceptionHandler();
|
||||||
|
app.UseStatusCodePages();
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseCors(CorsPolicyName);
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
// Endpoint de santé — utilisé par le healthcheck Docker Compose (depends_on: service_healthy).
|
||||||
|
app.MapHealthChecks("/health");
|
||||||
|
|
||||||
|
// --- Application automatique des migrations EF Core au démarrage ---
|
||||||
|
// Au premier lancement, la base (ex. /data/trajets.db sur le volume monté) est créée ;
|
||||||
|
// aux lancements suivants, les migrations en attente sont appliquées sans intervention manuelle.
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
await dbContext.Database.MigrateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
// Exposé pour les tests d'intégration (WebApplicationFactory<Program>).
|
||||||
|
public partial class Program { }
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:8200",
|
||||||
|
"sslPort": 44379
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5237",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:7085;http://localhost:5237",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implémentation EF Core de <see cref="IAdresseCacheRepository"/> au-dessus d'<see cref="AppDbContext"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class AdresseCacheRepository : IAdresseCacheRepository
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
|
public AdresseCacheRepository(AppDbContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AdresseCache>> RechercherAsync(
|
||||||
|
string fragmentNormalise,
|
||||||
|
int limite,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// La colonne Adresse stocke déjà la forme normalisée : la comparaison se fait donc
|
||||||
|
// directement sur celle-ci. EF.Functions.Like assure une comparaison indépendante
|
||||||
|
// de la casse côté SQLite.
|
||||||
|
var motif = $"%{fragmentNormalise}%";
|
||||||
|
|
||||||
|
return await _db.AdresseCache
|
||||||
|
.Where(a => EF.Functions.Like(a.Adresse, motif))
|
||||||
|
.OrderByDescending(a => a.DerniereUtilisation)
|
||||||
|
.Take(limite)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AdresseCache?> ObtenirParAdresseNormaliseeAsync(
|
||||||
|
string adresseNormalisee,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _db.AdresseCache
|
||||||
|
.FirstOrDefaultAsync(a => a.Adresse == adresseNormalisee, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AjouterAsync(AdresseCache entree, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _db.AdresseCache.AddAsync(entree, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> CompterAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return _db.AdresseCache.CountAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AdresseCache>> ObtenirPlusAnciennesAsync(
|
||||||
|
int nombre,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (nombre <= 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<AdresseCache>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _db.AdresseCache
|
||||||
|
.OrderBy(a => a.DerniereUtilisation)
|
||||||
|
.Take(nombre)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Supprimer(IEnumerable<AdresseCache> entrees)
|
||||||
|
{
|
||||||
|
_db.AdresseCache.RemoveRange(entrees);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> SauvegarderAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return _db.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Accès aux données du Cache_Adresses (table <c>AdresseCache</c>).
|
||||||
|
/// Toutes les correspondances d'adresse s'effectuent sur le texte normalisé
|
||||||
|
/// (voir <see cref="AdresseCacheService"/>).
|
||||||
|
/// </summary>
|
||||||
|
public interface IAdresseCacheRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne les entrées dont l'adresse normalisée contient le fragment normalisé fourni,
|
||||||
|
/// triées de la plus récemment utilisée à la plus ancienne, limitées à <paramref name="limite"/>.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<AdresseCache>> RechercherAsync(
|
||||||
|
string fragmentNormalise,
|
||||||
|
int limite,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>Retourne l'entrée correspondant exactement à l'adresse normalisée, ou <c>null</c>.</summary>
|
||||||
|
Task<AdresseCache?> ObtenirParAdresseNormaliseeAsync(
|
||||||
|
string adresseNormalisee,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>Ajoute une nouvelle entrée au cache.</summary>
|
||||||
|
Task AjouterAsync(AdresseCache entree, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>Compte le nombre total d'entrées du cache.</summary>
|
||||||
|
Task<int> CompterAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne les entrées les plus anciennes (par <c>DerniereUtilisation</c> croissante),
|
||||||
|
/// limitées à <paramref name="nombre"/>. Utilisé pour l'éviction LRU.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<AdresseCache>> ObtenirPlusAnciennesAsync(
|
||||||
|
int nombre,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>Supprime les entrées fournies du cache.</summary>
|
||||||
|
void Supprimer(IEnumerable<AdresseCache> entrees);
|
||||||
|
|
||||||
|
/// <summary>Persiste les modifications en attente.</summary>
|
||||||
|
Task<int> SauvegarderAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Accès aux données des trajets et gestion des positions au sein d'une journée.
|
||||||
|
/// <para>
|
||||||
|
/// La <see cref="Trajet.Position"/> est une valeur décimale (REAL) permettant l'insertion
|
||||||
|
/// sans réordonnancement systématique : ajout en fin de journée → <c>max(Position) + 1.0</c>,
|
||||||
|
/// insertion entre deux positions P₁ et P₂ → <c>(P₁ + P₂) / 2</c>. Lorsque l'écart devient
|
||||||
|
/// trop faible (< <see cref="SeuilRenumerotation"/>), la journée est renumérotée par pas de
|
||||||
|
/// 1.0 en transaction en préservant l'ordre relatif (Property 15, Exigences 11.7/11.8).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Ce repository fournit uniquement les primitives d'accès et de calcul de position ;
|
||||||
|
/// la logique métier (validation, calcul de distance, mise en cache) relève de TrajetService.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public interface ITrajetRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Seuil de précision en deçà duquel une renumérotation de la journée est déclenchée.
|
||||||
|
/// Conforme à la conception : écart < 0.001.
|
||||||
|
/// </summary>
|
||||||
|
const decimal SeuilRenumerotation = 0.001m;
|
||||||
|
|
||||||
|
/// <summary>Retourne le trajet par identifiant, ou <c>null</c> s'il n'existe pas.</summary>
|
||||||
|
Task<Trajet?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>Retourne les trajets d'une journée triés par <see cref="Trajet.Position"/> croissante.</summary>
|
||||||
|
Task<List<Trajet>> GetByDateAsync(DateOnly date, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Résout le véhicule applicable à la date du trajet et copie sa <c>PuissanceFiscale</c>
|
||||||
|
/// sur le trajet (dénormalisation, Exigence 10.14). Ne modifie ni <c>VehiculeId</c> ni
|
||||||
|
/// <c>PuissanceFiscale</c> si aucun véhicule n'est applicable à cette date.
|
||||||
|
/// Coordonné avec <see cref="Services.IVehiculeService.GetVehiculeADateAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
Task AppliquerVehiculeADateAsync(Trajet trajet, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcule la position d'un trajet ajouté en fin de journée : <c>max(Position) + 1.0</c>
|
||||||
|
/// pour la date, ou <c>1.0</c> si la journée est vide.
|
||||||
|
/// </summary>
|
||||||
|
Task<decimal> CalculerPositionFinDeJourneeAsync(DateOnly date, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcule la position d'un trajet inséré entre les positions <paramref name="positionPrecedente"/>
|
||||||
|
/// (P₁) et <paramref name="positionSuivante"/> (P₂) : <c>(P₁ + P₂) / 2</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentException">Si P₁ n'est pas strictement inférieure à P₂.</exception>
|
||||||
|
decimal CalculerPositionEntre(decimal positionPrecedente, decimal positionSuivante);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indique si l'écart entre deux positions adjacentes est devenu trop faible
|
||||||
|
/// (< <see cref="SeuilRenumerotation"/>) pour insérer une nouvelle valeur intermédiaire.
|
||||||
|
/// </summary>
|
||||||
|
bool NecessiteRenumerotation(decimal positionPrecedente, decimal positionSuivante);
|
||||||
|
|
||||||
|
/// <summary>Ajoute un trajet (sans persister). L'appelant gère la transaction et l'appel à <see cref="SauvegarderAsync"/>.</summary>
|
||||||
|
Task AddAsync(Trajet trajet, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>Marque un trajet existant comme modifié (sans persister).</summary>
|
||||||
|
void Update(Trajet trajet);
|
||||||
|
|
||||||
|
/// <summary>Supprime un trajet (sans persister).</summary>
|
||||||
|
void Delete(Trajet trajet);
|
||||||
|
|
||||||
|
/// <summary>Persiste les changements en attente.</summary>
|
||||||
|
Task<int> SauvegarderAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applique un réordonnancement manuel : met à jour uniquement la <see cref="Trajet.Position"/>
|
||||||
|
/// des trajets fournis (par identifiant) sans altérer leurs autres champs, en transaction.
|
||||||
|
/// Les identifiants inconnus sont ignorés.
|
||||||
|
/// </summary>
|
||||||
|
Task ReordonnerAsync(
|
||||||
|
IReadOnlyList<(int Id, decimal Position)> positions,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renumérote les trajets d'une journée par pas de 1.0 (1.0, 2.0, 3.0, …) en préservant
|
||||||
|
/// l'ordre relatif courant (tri par Position puis Id), en transaction. Retourne la liste
|
||||||
|
/// renumérotée triée par Position croissante (Property 15).
|
||||||
|
/// </summary>
|
||||||
|
Task<List<Trajet>> RenumeroterJourneeAsync(DateOnly date, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Accès aux données des véhicules et aux trajets impactés par une réaffectation.
|
||||||
|
/// L'historique des véhicules est conservé (aucune suppression) — Exigence 10.12 / Property 22.
|
||||||
|
/// </summary>
|
||||||
|
public interface IVehiculeRepository
|
||||||
|
{
|
||||||
|
/// <summary>Retourne l'historique complet des véhicules, trié par Date_Effet décroissante puis Id décroissant.</summary>
|
||||||
|
Task<List<Vehicule>> GetHistoriqueAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>Retourne le véhicule par identifiant, ou <c>null</c> s'il n'existe pas.</summary>
|
||||||
|
Task<Vehicule?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Résout le véhicule courant à une date donnée : Date_Effet la plus récente parmi
|
||||||
|
/// celles <= <paramref name="date"/> (ORDER BY DateEffet DESC, Id DESC LIMIT 1).
|
||||||
|
/// Retourne <c>null</c> si aucun véhicule n'a de Date_Effet <= <paramref name="date"/>.
|
||||||
|
/// </summary>
|
||||||
|
Task<Vehicule?> GetVehiculeCourantAsync(DateOnly date, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>Ajoute un nouveau véhicule (sans persister). L'appelant gère la transaction.</summary>
|
||||||
|
Task AddAsync(Vehicule vehicule, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne les trajets dont la Date est >= <paramref name="dateEffet"/> et dont la
|
||||||
|
/// PuissanceFiscale diffère de <paramref name="puissanceFiscale"/> (candidats à la réaffectation).
|
||||||
|
/// </summary>
|
||||||
|
Task<List<Trajet>> GetTrajetsAReaffecterAsync(
|
||||||
|
DateOnly dateEffet,
|
||||||
|
int puissanceFiscale,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
using TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Repositories;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class TrajetRepository : ITrajetRepository
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
private readonly IVehiculeService _vehiculeService;
|
||||||
|
|
||||||
|
public TrajetRepository(AppDbContext context, IVehiculeService vehiculeService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_vehiculeService = vehiculeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<Trajet?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Trajets
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<Trajet>> GetByDateAsync(DateOnly date, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// SQLite ne sait pas trier (ORDER BY) sur une colonne decimal : on ramène les
|
||||||
|
// trajets du jour puis on trie côté client (volume par journée très réduit).
|
||||||
|
var trajets = await _context.Trajets
|
||||||
|
.Where(t => t.Date == date)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return trajets
|
||||||
|
.OrderBy(t => t.Position)
|
||||||
|
.ThenBy(t => t.Id)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task AppliquerVehiculeADateAsync(Trajet trajet, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Résolution du véhicule applicable à la date du trajet (Date_Effet la plus récente <= Date).
|
||||||
|
var vehicule = await _vehiculeService.GetVehiculeADateAsync(trajet.Date, cancellationToken);
|
||||||
|
if (vehicule is null)
|
||||||
|
{
|
||||||
|
// Aucun véhicule applicable à cette date : on laisse la logique métier (TrajetService) décider.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dénormalisation : copie de la puissance fiscale du véhicule au moment de l'association (Exigence 10.14).
|
||||||
|
trajet.VehiculeId = vehicule.Id;
|
||||||
|
trajet.PuissanceFiscale = vehicule.PuissanceFiscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<decimal> CalculerPositionFinDeJourneeAsync(DateOnly date, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// SQLite ne sait pas appliquer MAX sur une colonne decimal : on ramène les positions
|
||||||
|
// du jour et on calcule le maximum côté client (volume par journée très réduit).
|
||||||
|
var positions = await _context.Trajets
|
||||||
|
.Where(t => t.Date == date)
|
||||||
|
.Select(t => t.Position)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (positions.Count == 0)
|
||||||
|
{
|
||||||
|
return 1.0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
return positions.Max() + 1.0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public decimal CalculerPositionEntre(decimal positionPrecedente, decimal positionSuivante)
|
||||||
|
{
|
||||||
|
if (positionPrecedente >= positionSuivante)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"La position précédente ({positionPrecedente}) doit être strictement inférieure à la position suivante ({positionSuivante}).",
|
||||||
|
nameof(positionPrecedente));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (positionPrecedente + positionSuivante) / 2.0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool NecessiteRenumerotation(decimal positionPrecedente, decimal positionSuivante)
|
||||||
|
{
|
||||||
|
return positionSuivante - positionPrecedente < ITrajetRepository.SeuilRenumerotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddAsync(Trajet trajet, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_context.Trajets.Add(trajet);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Update(Trajet trajet)
|
||||||
|
{
|
||||||
|
_context.Trajets.Update(trajet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Delete(Trajet trajet)
|
||||||
|
{
|
||||||
|
_context.Trajets.Remove(trajet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<int> SauvegarderAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task ReordonnerAsync(
|
||||||
|
IReadOnlyList<(int Id, decimal Position)> positions,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (positions.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ids = positions.Select(p => p.Id).ToList();
|
||||||
|
var trajets = await _context.Trajets
|
||||||
|
.Where(t => ids.Contains(t.Id))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var positionParId = positions.ToDictionary(p => p.Id, p => p.Position);
|
||||||
|
|
||||||
|
foreach (var trajet in trajets)
|
||||||
|
{
|
||||||
|
if (positionParId.TryGetValue(trajet.Id, out var nouvellePosition))
|
||||||
|
{
|
||||||
|
// Seule la Position est modifiée ; aucun autre champ n'est altéré (Exigence 11.9).
|
||||||
|
trajet.Position = nouvellePosition;
|
||||||
|
trajet.UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<Trajet>> RenumeroterJourneeAsync(DateOnly date, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Préservation de l'ordre relatif courant (Position croissante, Id en départage) — Property 15.
|
||||||
|
var trajets = (await _context.Trajets
|
||||||
|
.Where(t => t.Date == date)
|
||||||
|
.ToListAsync(cancellationToken))
|
||||||
|
.OrderBy(t => t.Position)
|
||||||
|
.ThenBy(t => t.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
decimal position = 1.0m;
|
||||||
|
foreach (var trajet in trajets)
|
||||||
|
{
|
||||||
|
trajet.Position = position;
|
||||||
|
trajet.UpdatedAt = DateTime.UtcNow;
|
||||||
|
position += 1.0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
|
||||||
|
return trajets;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Repositories;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class VehiculeRepository : IVehiculeRepository
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
|
||||||
|
public VehiculeRepository(AppDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<Vehicule>> GetHistoriqueAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Vehicules
|
||||||
|
.OrderByDescending(v => v.DateEffet)
|
||||||
|
.ThenByDescending(v => v.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<Vehicule?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Vehicules
|
||||||
|
.FirstOrDefaultAsync(v => v.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<Vehicule?> GetVehiculeCourantAsync(DateOnly date, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Design SQL : SELECT * FROM Vehicules WHERE DateEffet <= date ORDER BY DateEffet DESC, Id DESC LIMIT 1
|
||||||
|
return await _context.Vehicules
|
||||||
|
.Where(v => v.DateEffet <= date)
|
||||||
|
.OrderByDescending(v => v.DateEffet)
|
||||||
|
.ThenByDescending(v => v.Id)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddAsync(Vehicule vehicule, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_context.Vehicules.Add(vehicule);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<Trajet>> GetTrajetsAReaffecterAsync(
|
||||||
|
DateOnly dateEffet,
|
||||||
|
int puissanceFiscale,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Trajets
|
||||||
|
.Where(t => t.Date >= dateEffet && t.PuissanceFiscale != puissanceFiscale)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
using TrajetKmTracker.Api.Repositories;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implémentation du Cache_Adresses. Les adresses sont stockées et comparées sous forme
|
||||||
|
/// normalisée (trim + minuscules + suppression des accents) afin d'assurer un matching robuste.
|
||||||
|
/// Les écritures (upsert + éviction LRU) sont exécutées en transaction.
|
||||||
|
/// </summary>
|
||||||
|
public class AdresseCacheService : IAdresseCacheService
|
||||||
|
{
|
||||||
|
/// <summary>Longueur minimale d'une requête de recherche locale (Requirement 9.3).</summary>
|
||||||
|
private const int LongueurMinimaleRecherche = 3;
|
||||||
|
|
||||||
|
/// <summary>Taille maximale du cache avant éviction LRU (Requirement 9.5).</summary>
|
||||||
|
private const int TailleMaximaleCache = 1_000;
|
||||||
|
|
||||||
|
private readonly IAdresseCacheRepository _repository;
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
|
public AdresseCacheService(IAdresseCacheRepository repository, AppDbContext db)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AdresseCacheDto>> RechercherLocalAsync(
|
||||||
|
string query,
|
||||||
|
int limite = 5,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var fragment = Normaliser(query);
|
||||||
|
if (fragment.Length < LongueurMinimaleRecherche)
|
||||||
|
{
|
||||||
|
return Array.Empty<AdresseCacheDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limite <= 0)
|
||||||
|
{
|
||||||
|
limite = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entrees = await _repository.RechercherAsync(fragment, limite, cancellationToken);
|
||||||
|
|
||||||
|
return entrees
|
||||||
|
.Select(e => new AdresseCacheDto(e.Adresse, e.Latitude, e.Longitude))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AdresseCacheEntreeDto?> ObtenirParAdresseAsync(
|
||||||
|
string adresse,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var normalisee = Normaliser(adresse);
|
||||||
|
if (normalisee.Length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entree = await _repository.ObtenirParAdresseNormaliseeAsync(normalisee, cancellationToken);
|
||||||
|
if (entree is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toute utilisation d'une adresse en cache rafraîchit sa date de dernière utilisation.
|
||||||
|
// Si une transaction est déjà ouverte par l'appelant (ex. création d'un Trajet),
|
||||||
|
// on y participe sans en ouvrir une nouvelle : SQLite interdit les transactions imbriquées.
|
||||||
|
entree.DerniereUtilisation = DateTime.UtcNow;
|
||||||
|
if (_db.Database.CurrentTransaction is null)
|
||||||
|
{
|
||||||
|
await using var transaction = await _db.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
await _repository.SauvegarderAsync(cancellationToken);
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _repository.SauvegarderAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AdresseCacheEntreeDto(
|
||||||
|
entree.Adresse,
|
||||||
|
entree.Latitude,
|
||||||
|
entree.Longitude,
|
||||||
|
entree.GeometrieJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAdresseAsync(
|
||||||
|
string adresse,
|
||||||
|
double latitude,
|
||||||
|
double longitude,
|
||||||
|
string? geometrieJson = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var normalisee = Normaliser(adresse);
|
||||||
|
if (normalisee.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si une transaction est déjà ouverte par l'appelant (ex. TrajetService.CreateAsync),
|
||||||
|
// on y participe sans en ouvrir une nouvelle (SQLite interdit les transactions imbriquées).
|
||||||
|
var transactionAmbiante = _db.Database.CurrentTransaction is not null;
|
||||||
|
var transaction = transactionAmbiante
|
||||||
|
? null
|
||||||
|
: await _db.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
|
||||||
|
var existante = await _repository.ObtenirParAdresseNormaliseeAsync(normalisee, cancellationToken);
|
||||||
|
if (existante is not null)
|
||||||
|
{
|
||||||
|
// Adresse déjà présente : coordonnées inchangées, seule la date d'utilisation est
|
||||||
|
// rafraîchie (Requirements 9.1, 9.6).
|
||||||
|
existante.DerniereUtilisation = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Nouvelle adresse : insertion avec les coordonnées fournies.
|
||||||
|
var nouvelle = new AdresseCache
|
||||||
|
{
|
||||||
|
Adresse = normalisee,
|
||||||
|
Latitude = latitude,
|
||||||
|
Longitude = longitude,
|
||||||
|
GeometrieJson = geometrieJson,
|
||||||
|
DerniereUtilisation = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
await _repository.AjouterAsync(nouvelle, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _repository.SauvegarderAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Éviction LRU : conserver au plus les 1 000 entrées les plus récemment utilisées
|
||||||
|
// et supprimer les plus anciennes (Requirement 9.5).
|
||||||
|
var total = await _repository.CompterAsync(cancellationToken);
|
||||||
|
if (total > TailleMaximaleCache)
|
||||||
|
{
|
||||||
|
var aSupprimer = await _repository.ObtenirPlusAnciennesAsync(
|
||||||
|
total - TailleMaximaleCache, cancellationToken);
|
||||||
|
|
||||||
|
_repository.Supprimer(aSupprimer);
|
||||||
|
await _repository.SauvegarderAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On ne valide que la transaction que l'on a nous-mêmes ouverte ; sinon c'est
|
||||||
|
// l'appelant qui validera la transaction ambiante.
|
||||||
|
if (transaction is not null)
|
||||||
|
{
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
await transaction.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalise un texte d'adresse : suppression des espaces de bord, passage en minuscules
|
||||||
|
/// et suppression des accents (diacritiques). Cohérent avec la normalisation des Adresses_Nommées.
|
||||||
|
/// </summary>
|
||||||
|
private static string Normaliser(string? valeur)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(valeur))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmee = valeur.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
var decomposee = trimmee.Normalize(NormalizationForm.FormD);
|
||||||
|
var builder = new StringBuilder(decomposee.Length);
|
||||||
|
foreach (var caractere in decomposee)
|
||||||
|
{
|
||||||
|
var categorie = CharUnicodeInfo.GetUnicodeCategory(caractere);
|
||||||
|
if (categorie != UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
builder.Append(caractere);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString().Normalize(NormalizationForm.FormC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implémentation du Carnet_Adresses (Exigence 15). Gère le CRUD des Adresses_Nommées avec :
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>normalisation de la Dénomination (trim + minuscules + suppression des accents) pour
|
||||||
|
/// garantir l'unicité (15.6/15.7) ;</item>
|
||||||
|
/// <item>réutilisation du Cache_Adresses si l'adresse normalisée y est présente (15.10),
|
||||||
|
/// sinon géocodage/re-géocodage via l'API BAN avec un délai de 5 s (15.11) ;</item>
|
||||||
|
/// <item>persistance sans coordonnées + <c>GeocodageReussi = false</c> si BAN est
|
||||||
|
/// indisponible ou dépasse le délai (15.12) ;</item>
|
||||||
|
/// <item>mise en cache des coordonnées obtenues (15.9).</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public class AdresseNommeeService : IAdresseNommeeService
|
||||||
|
{
|
||||||
|
private const int DenominationMaxLength = 100;
|
||||||
|
private const int AdresseMaxLength = 255;
|
||||||
|
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly IBANClient _banClient;
|
||||||
|
private readonly IAdresseCacheService _cache;
|
||||||
|
|
||||||
|
public AdresseNommeeService(AppDbContext db, IBANClient banClient, IAdresseCacheService cache)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_banClient = banClient;
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<AdresseNommeeDto>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var adresses = await _db.AdressesNommees
|
||||||
|
.AsNoTracking()
|
||||||
|
.OrderBy(a => a.Denomination)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return adresses.Select(ToDto).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<AdresseNommeeDto> CreateAsync(
|
||||||
|
SaveAdresseNommeeDto dto,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ValiderDenomination(dto.Denomination);
|
||||||
|
ValiderAdresse(dto.Adresse);
|
||||||
|
|
||||||
|
var denominationNormalisee = Normaliser(dto.Denomination);
|
||||||
|
|
||||||
|
// Contrôle d'unicité avant persistance (15.7 / Property 28) ; l'index unique
|
||||||
|
// UX_AdresseNommee_Denomination reste le dernier rempart au niveau SQL.
|
||||||
|
var collision = await _db.AdressesNommees
|
||||||
|
.AnyAsync(a => a.DenominationNormalisee == denominationNormalisee, cancellationToken);
|
||||||
|
if (collision)
|
||||||
|
{
|
||||||
|
throw new DenominationDupliqueeException(
|
||||||
|
$"La dénomination « {dto.Denomination.Trim()} » est déjà utilisée.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var (latitude, longitude, geocodageReussi) =
|
||||||
|
await ResoudreCoordonneesAsync(dto.Adresse, dto.Latitude, dto.Longitude, cancellationToken);
|
||||||
|
|
||||||
|
var maintenant = DateTime.UtcNow;
|
||||||
|
var entite = new AdresseNommee
|
||||||
|
{
|
||||||
|
Denomination = dto.Denomination.Trim(),
|
||||||
|
DenominationNormalisee = denominationNormalisee,
|
||||||
|
Adresse = dto.Adresse.Trim(),
|
||||||
|
Latitude = latitude,
|
||||||
|
Longitude = longitude,
|
||||||
|
GeocodageReussi = geocodageReussi,
|
||||||
|
CreatedAt = maintenant,
|
||||||
|
UpdatedAt = maintenant
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var transaction = await _db.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_db.AdressesNommees.Add(entite);
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
// Dernier rempart : violation de l'index unique (course concurrente).
|
||||||
|
throw new DenominationDupliqueeException(
|
||||||
|
$"La dénomination « {dto.Denomination.Trim()} » est déjà utilisée.", ex);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise en cache des coordonnées obtenues si absentes du Cache_Adresses (15.9).
|
||||||
|
if (geocodageReussi && latitude.HasValue && longitude.HasValue)
|
||||||
|
{
|
||||||
|
await _cache.UpsertAdresseAsync(entite.Adresse, latitude.Value, longitude.Value,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToDto(entite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<AdresseNommeeDto> UpdateAsync(
|
||||||
|
int id,
|
||||||
|
SaveAdresseNommeeDto dto,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ValiderDenomination(dto.Denomination);
|
||||||
|
ValiderAdresse(dto.Adresse);
|
||||||
|
|
||||||
|
var entite = await _db.AdressesNommees.FirstOrDefaultAsync(a => a.Id == id, cancellationToken)
|
||||||
|
?? throw new AdresseNommeeNotFoundException(
|
||||||
|
$"L'adresse nommée d'identifiant {id} est introuvable.");
|
||||||
|
|
||||||
|
var denominationNormalisee = Normaliser(dto.Denomination);
|
||||||
|
|
||||||
|
// Unicité : une autre Adresse_Nommee ne doit pas porter la même Dénomination normalisée (15.7).
|
||||||
|
var collision = await _db.AdressesNommees
|
||||||
|
.AnyAsync(a => a.Id != id && a.DenominationNormalisee == denominationNormalisee, cancellationToken);
|
||||||
|
if (collision)
|
||||||
|
{
|
||||||
|
throw new DenominationDupliqueeException(
|
||||||
|
$"La dénomination « {dto.Denomination.Trim()} » est déjà utilisée.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var adresseTrim = dto.Adresse.Trim();
|
||||||
|
var adresseAChange = !string.Equals(
|
||||||
|
Normaliser(entite.Adresse), Normaliser(adresseTrim), StringComparison.Ordinal);
|
||||||
|
|
||||||
|
double? latitude = entite.Latitude;
|
||||||
|
double? longitude = entite.Longitude;
|
||||||
|
var geocodageReussi = entite.GeocodageReussi;
|
||||||
|
|
||||||
|
// Re-géocodage uniquement si le texte d'adresse a changé (15.11).
|
||||||
|
if (adresseAChange)
|
||||||
|
{
|
||||||
|
(latitude, longitude, geocodageReussi) =
|
||||||
|
await ResoudreCoordonneesAsync(adresseTrim, dto.Latitude, dto.Longitude, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
entite.Denomination = dto.Denomination.Trim();
|
||||||
|
entite.DenominationNormalisee = denominationNormalisee;
|
||||||
|
entite.Adresse = adresseTrim;
|
||||||
|
entite.Latitude = latitude;
|
||||||
|
entite.Longitude = longitude;
|
||||||
|
entite.GeocodageReussi = geocodageReussi;
|
||||||
|
entite.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await using var transaction = await _db.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw new DenominationDupliqueeException(
|
||||||
|
$"La dénomination « {dto.Denomination.Trim()} » est déjà utilisée.", ex);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geocodageReussi && latitude.HasValue && longitude.HasValue)
|
||||||
|
{
|
||||||
|
await _cache.UpsertAdresseAsync(entite.Adresse, latitude.Value, longitude.Value,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToDto(entite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task DeleteAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var entite = await _db.AdressesNommees.FirstOrDefaultAsync(a => a.Id == id, cancellationToken)
|
||||||
|
?? throw new AdresseNommeeNotFoundException(
|
||||||
|
$"L'adresse nommée d'identifiant {id} est introuvable.");
|
||||||
|
|
||||||
|
// Suppression du seul enregistrement du Carnet_Adresses : les Trajets ne référencent que
|
||||||
|
// le texte d'adresse et les coordonnées copiés, donc rien d'autre n'est modifié (15.14).
|
||||||
|
await using var transaction = await _db.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_db.AdressesNommees.Remove(entite);
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Résout les coordonnées d'un texte d'adresse : réutilisation du Cache_Adresses si l'adresse
|
||||||
|
/// normalisée y figure (15.10), sinon coordonnées fournies par le frontend (sélection BAN),
|
||||||
|
/// sinon géocodage via l'API BAN (15.11). En cas d'indisponibilité/timeout de la BAN, aucune
|
||||||
|
/// coordonnée n'est retournée et <c>GeocodageReussi</c> vaut <c>false</c> (15.12).
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(double? Latitude, double? Longitude, bool GeocodageReussi)> ResoudreCoordonneesAsync(
|
||||||
|
string adresse,
|
||||||
|
double? latitudeFournie,
|
||||||
|
double? longitudeFournie,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1) Réutilisation du Cache_Adresses sans appel BAN (15.10).
|
||||||
|
var enCache = await _cache.ObtenirParAdresseAsync(adresse, cancellationToken);
|
||||||
|
if (enCache is not null)
|
||||||
|
{
|
||||||
|
return (enCache.Latitude, enCache.Longitude, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Coordonnées déjà résolues côté frontend via une sélection BAN : on les conserve
|
||||||
|
// directement, ce qui évite un appel BAN redondant (15.5).
|
||||||
|
if (latitudeFournie.HasValue && longitudeFournie.HasValue)
|
||||||
|
{
|
||||||
|
return (latitudeFournie, longitudeFournie, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Géocodage via l'API BAN (timeout 5 s configuré sur le HttpClient) (15.11).
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resultats = await _banClient.RechercherAsync(adresse, cancellationToken);
|
||||||
|
var premier = resultats.FirstOrDefault();
|
||||||
|
if (premier is not null)
|
||||||
|
{
|
||||||
|
return (premier.Latitude, premier.Longitude, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aucune correspondance BAN : persistance sans coordonnées.
|
||||||
|
return (null, null, false);
|
||||||
|
}
|
||||||
|
catch (BANIndisponibleException)
|
||||||
|
{
|
||||||
|
// BAN indisponible/timeout : persistance sans coordonnées (15.12).
|
||||||
|
return (null, null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValiderDenomination(string? denomination)
|
||||||
|
{
|
||||||
|
var trimmee = denomination?.Trim() ?? string.Empty;
|
||||||
|
if (trimmee.Length == 0 || trimmee.Length > DenominationMaxLength)
|
||||||
|
{
|
||||||
|
throw new DenominationInvalideException(
|
||||||
|
"La dénomination est absente ou invalide (1 à 100 caractères requis).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValiderAdresse(string? adresse)
|
||||||
|
{
|
||||||
|
var trimmee = adresse?.Trim() ?? string.Empty;
|
||||||
|
if (trimmee.Length == 0 || trimmee.Length > AdresseMaxLength)
|
||||||
|
{
|
||||||
|
throw new AdresseNommeeInvalideException(
|
||||||
|
"L'adresse est absente ou invalide (1 à 255 caractères requis).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AdresseNommeeDto ToDto(AdresseNommee a) =>
|
||||||
|
new(a.Id, a.Denomination, a.Adresse, a.Latitude, a.Longitude);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalise un texte : suppression des espaces de bord, passage en minuscules et suppression
|
||||||
|
/// des accents (diacritiques). Cohérent avec <see cref="AdresseCacheService"/> afin d'assurer
|
||||||
|
/// un matching identique entre Carnet_Adresses et Cache_Adresses.
|
||||||
|
/// </summary>
|
||||||
|
private static string Normaliser(string? valeur)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(valeur))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmee = valeur.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
var decomposee = trimmee.Normalize(NormalizationForm.FormD);
|
||||||
|
var builder = new StringBuilder(decomposee.Length);
|
||||||
|
foreach (var caractere in decomposee)
|
||||||
|
{
|
||||||
|
var categorie = CharUnicodeInfo.GetUnicodeCategory(caractere);
|
||||||
|
if (categorie != UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
builder.Append(caractere);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString().Normalize(NormalizationForm.FormC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client typé de l'API BAN. Appelle <c>GET /search/?q={query}&limit=5</c>,
|
||||||
|
/// parse la réponse GeoJSON (FeatureCollection) et mappe les features en
|
||||||
|
/// <see cref="BANAdresseDto"/>. Lève <see cref="BANIndisponibleException"/>
|
||||||
|
/// en cas d'échec ou de timeout.
|
||||||
|
/// </summary>
|
||||||
|
public class BANClient : IBANClient
|
||||||
|
{
|
||||||
|
private const int MaxResultats = 5;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public BANClient(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<BANAdresseDto>> RechercherAsync(
|
||||||
|
string query,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
|
{
|
||||||
|
return Array.Empty<BANAdresseDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestUri = $"/search/?q={Uri.EscapeDataString(query)}&limit={MaxResultats}";
|
||||||
|
|
||||||
|
BANFeatureCollection? collection;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await _httpClient.GetAsync(requestUri, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
collection = await response.Content.ReadFromJsonAsync<BANFeatureCollection>(
|
||||||
|
JsonOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Le timeout configuré sur le HttpClient déclenche une annulation
|
||||||
|
// non demandée par l'appelant.
|
||||||
|
throw new BANIndisponibleException(
|
||||||
|
"L'appel à l'API BAN a dépassé le délai imparti (timeout).");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
throw new BANIndisponibleException(
|
||||||
|
"L'API BAN est indisponible ou a renvoyé une réponse en erreur.", ex);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new BANIndisponibleException(
|
||||||
|
"La réponse de l'API BAN n'a pas pu être interprétée.", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection?.Features is null || collection.Features.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<BANAdresseDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultats = new List<BANAdresseDto>(collection.Features.Count);
|
||||||
|
foreach (var feature in collection.Features)
|
||||||
|
{
|
||||||
|
var coordinates = feature.Geometry?.Coordinates;
|
||||||
|
if (coordinates is null || coordinates.Count < 2)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeoJSON : coordinates = [longitude, latitude].
|
||||||
|
var longitude = coordinates[0];
|
||||||
|
var latitude = coordinates[1];
|
||||||
|
|
||||||
|
resultats.Add(new BANAdresseDto(
|
||||||
|
Label: feature.Properties?.Label ?? string.Empty,
|
||||||
|
Latitude: latitude,
|
||||||
|
Longitude: longitude,
|
||||||
|
Postcode: feature.Properties?.Postcode ?? string.Empty,
|
||||||
|
City: feature.Properties?.City ?? string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultats;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Modèles internes de désérialisation de la réponse GeoJSON BAN ---
|
||||||
|
|
||||||
|
private sealed class BANFeatureCollection
|
||||||
|
{
|
||||||
|
[JsonPropertyName("features")]
|
||||||
|
public List<BANFeature>? Features { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class BANFeature
|
||||||
|
{
|
||||||
|
[JsonPropertyName("properties")]
|
||||||
|
public BANProperties? Properties { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("geometry")]
|
||||||
|
public BANGeometry? Geometry { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class BANProperties
|
||||||
|
{
|
||||||
|
[JsonPropertyName("label")]
|
||||||
|
public string? Label { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("postcode")]
|
||||||
|
public string? Postcode { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("city")]
|
||||||
|
public string? City { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class BANGeometry
|
||||||
|
{
|
||||||
|
// [longitude, latitude]
|
||||||
|
[JsonPropertyName("coordinates")]
|
||||||
|
public List<double>? Coordinates { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Résultat du calcul du montant barème pour un (année, puissance fiscale, total km) donné.
|
||||||
|
/// Lorsque aucune tranche n'est configurée, <see cref="BaremeConfigure"/> vaut <c>false</c>,
|
||||||
|
/// le <see cref="Montant"/> est nul et un <see cref="Avertissement"/> décrit la situation
|
||||||
|
/// (Exigence 13.4) — le calcul n'est jamais interrompu par une exception bloquante.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Montant">Montant calculé : total_km × Coefficient + Base de la tranche, ou 0 si non configuré.</param>
|
||||||
|
/// <param name="BaremeConfigure">Vrai si une tranche a été trouvée et appliquée.</param>
|
||||||
|
/// <param name="Coefficient">Coefficient €/km de la tranche appliquée (0 si non configuré).</param>
|
||||||
|
/// <param name="Base">Base fixe € de la tranche appliquée (0 si non configuré).</param>
|
||||||
|
/// <param name="Avertissement">Message d'avertissement lorsque le barème n'est pas configuré, sinon <c>null</c>.</param>
|
||||||
|
public record BaremeCalculResult(
|
||||||
|
decimal Montant,
|
||||||
|
bool BaremeConfigure,
|
||||||
|
decimal Coefficient,
|
||||||
|
decimal Base,
|
||||||
|
string? Avertissement);
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class BaremeService : IBaremeService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
|
||||||
|
public BaremeService(AppDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<TrancheBaremeDto>> GetBaremeAsync(
|
||||||
|
int annee, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// KmMin est un decimal : SQLite ne peut pas l'utiliser dans un ORDER BY, on trie côté client.
|
||||||
|
var tranches = (await _context.TranchesBareme
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.Annee == annee)
|
||||||
|
.ToListAsync(cancellationToken))
|
||||||
|
.OrderBy(t => t.PuissanceFiscale)
|
||||||
|
.ThenBy(t => t.KmMin)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return tranches.Select(ToDto).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<TrancheBaremeDto> AddTrancheAsync(
|
||||||
|
SaveTrancheBaremeDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await EnsureNoOverlapAsync(dto.Annee, dto.PuissanceFiscale, dto.KmMin, dto.KmMax, excludeId: null, cancellationToken);
|
||||||
|
|
||||||
|
var tranche = new TrancheBareme
|
||||||
|
{
|
||||||
|
Annee = dto.Annee,
|
||||||
|
PuissanceFiscale = dto.PuissanceFiscale,
|
||||||
|
KmMin = dto.KmMin,
|
||||||
|
KmMax = dto.KmMax,
|
||||||
|
Coefficient = dto.Coefficient,
|
||||||
|
Base = dto.Base,
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.TranchesBareme.Add(tranche);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ToDto(tranche);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<TrancheBaremeDto> UpdateTrancheAsync(
|
||||||
|
int id, SaveTrancheBaremeDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tranche = await _context.TranchesBareme
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == id, cancellationToken)
|
||||||
|
?? throw new TrancheBaremeNotFoundException($"La tranche de barème d'identifiant {id} est introuvable.");
|
||||||
|
|
||||||
|
await EnsureNoOverlapAsync(dto.Annee, dto.PuissanceFiscale, dto.KmMin, dto.KmMax, excludeId: id, cancellationToken);
|
||||||
|
|
||||||
|
tranche.Annee = dto.Annee;
|
||||||
|
tranche.PuissanceFiscale = dto.PuissanceFiscale;
|
||||||
|
tranche.KmMin = dto.KmMin;
|
||||||
|
tranche.KmMax = dto.KmMax;
|
||||||
|
tranche.Coefficient = dto.Coefficient;
|
||||||
|
tranche.Base = dto.Base;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ToDto(tranche);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task DeleteTrancheAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tranche = await _context.TranchesBareme
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == id, cancellationToken)
|
||||||
|
?? throw new TrancheBaremeNotFoundException($"La tranche de barème d'identifiant {id} est introuvable.");
|
||||||
|
|
||||||
|
_context.TranchesBareme.Remove(tranche);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<BaremeCalculResult> CalculerMontantAsync(
|
||||||
|
int annee, int puissanceFiscale, decimal totalKm, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tranches = await _context.TranchesBareme
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.Annee == annee && t.PuissanceFiscale == puissanceFiscale)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Aucune tranche configurée : montant zéro + signal (Exigence 13.4), sans erreur bloquante.
|
||||||
|
if (tranches.Count == 0)
|
||||||
|
{
|
||||||
|
return new BaremeCalculResult(
|
||||||
|
Montant: 0m,
|
||||||
|
BaremeConfigure: false,
|
||||||
|
Coefficient: 0m,
|
||||||
|
Base: 0m,
|
||||||
|
Avertissement: $"Aucun barème configuré pour la puissance fiscale {puissanceFiscale} CV et l'année {annee} ; le montant de remboursement est nul.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sélection de la tranche dont [KmMin, KmMax[ contient le total annuel (Property 24/27).
|
||||||
|
// Borne max EXCLUSIVE ; KmMax == null = illimité. Les tranches ne se chevauchant pas,
|
||||||
|
// au plus une est sélectionnable.
|
||||||
|
var tranche = tranches.FirstOrDefault(t =>
|
||||||
|
totalKm >= t.KmMin && (t.KmMax == null || totalKm < t.KmMax.Value));
|
||||||
|
|
||||||
|
if (tranche is null)
|
||||||
|
{
|
||||||
|
// Des tranches existent mais aucune ne couvre ce total : traité comme non couvert (montant nul).
|
||||||
|
return new BaremeCalculResult(
|
||||||
|
Montant: 0m,
|
||||||
|
BaremeConfigure: false,
|
||||||
|
Coefficient: 0m,
|
||||||
|
Base: 0m,
|
||||||
|
Avertissement: $"Aucune tranche du barème ne couvre un total de {totalKm} km pour la puissance fiscale {puissanceFiscale} CV et l'année {annee} ; le montant de remboursement est nul.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var montant = totalKm * tranche.Coefficient + tranche.Base;
|
||||||
|
|
||||||
|
return new BaremeCalculResult(
|
||||||
|
Montant: montant,
|
||||||
|
BaremeConfigure: true,
|
||||||
|
Coefficient: tranche.Coefficient,
|
||||||
|
Base: tranche.Base,
|
||||||
|
Avertissement: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<int> CopierDernierBaremeAsync(int anneeCible, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Année source = année la plus récente STRICTEMENT antérieure à l'année cible qui a un barème.
|
||||||
|
var anneesAvecBareme = (await _context.TranchesBareme
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(t => t.Annee)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync(cancellationToken))
|
||||||
|
.Where(a => a < anneeCible)
|
||||||
|
.OrderByDescending(a => a)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (anneesAvecBareme.Count == 0)
|
||||||
|
{
|
||||||
|
return 0; // Aucun barème antérieur : rien à recopier.
|
||||||
|
}
|
||||||
|
|
||||||
|
var anneeSource = anneesAvecBareme.First();
|
||||||
|
|
||||||
|
var source = await _context.TranchesBareme
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.Annee == anneeSource)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Annule et remplace : on supprime d'abord les tranches existantes de l'année cible.
|
||||||
|
var existantes = await _context.TranchesBareme
|
||||||
|
.Where(t => t.Annee == anneeCible)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
if (existantes.Count > 0)
|
||||||
|
{
|
||||||
|
_context.TranchesBareme.RemoveRange(existantes);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copie des tranches de l'année source vers l'année cible.
|
||||||
|
var copies = source.Select(t => new TrancheBareme
|
||||||
|
{
|
||||||
|
Annee = anneeCible,
|
||||||
|
PuissanceFiscale = t.PuissanceFiscale,
|
||||||
|
KmMin = t.KmMin,
|
||||||
|
KmMax = t.KmMax,
|
||||||
|
Coefficient = t.Coefficient,
|
||||||
|
Base = t.Base,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
_context.TranchesBareme.AddRange(copies);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
return copies.Count;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vérifie qu'aucune tranche existante pour la même (année, puissance fiscale) ne chevauche
|
||||||
|
/// l'intervalle [<paramref name="kmMin"/>, <paramref name="kmMax"/>]. La tranche d'identifiant
|
||||||
|
/// <paramref name="excludeId"/> (en cours de mise à jour) est exclue de la comparaison.
|
||||||
|
/// Deux intervalles [a1,a2] et [b1,b2] se chevauchent ssi a1 <= b2 ET b1 <= a2.
|
||||||
|
/// </summary>
|
||||||
|
private async Task EnsureNoOverlapAsync(
|
||||||
|
int annee, int puissanceFiscale, decimal kmMin, decimal? kmMax, int? excludeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var existantes = await _context.TranchesBareme
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.Annee == annee
|
||||||
|
&& t.PuissanceFiscale == puissanceFiscale
|
||||||
|
&& (excludeId == null || t.Id != excludeId.Value))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Convention [KmMin, KmMax[ (borne max EXCLUSIVE). KmMax == null = illimité (+infini).
|
||||||
|
// Deux intervalles [a1,a2[ et [b1,b2[ se chevauchent ssi a1 < b2 ET b1 < a2,
|
||||||
|
// en traitant une borne max nulle comme +infini.
|
||||||
|
var chevauche = existantes.Any(t =>
|
||||||
|
{
|
||||||
|
var aMaxInfini = kmMax == null;
|
||||||
|
var bMaxInfini = t.KmMax == null;
|
||||||
|
var condA = bMaxInfini || kmMin < t.KmMax!.Value; // a1 < b2
|
||||||
|
var condB = aMaxInfini || t.KmMin < kmMax!.Value; // b1 < a2
|
||||||
|
return condA && condB;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (chevauche)
|
||||||
|
{
|
||||||
|
throw new TranchesBaremeChevauchantesException(
|
||||||
|
"Les tranches du barème pour cette puissance fiscale et cette année se chevauchent.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<int>> GetAnneesAvecBaremeAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var annees = await _context.TranchesBareme
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(t => t.Annee)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
annees.Sort();
|
||||||
|
return annees;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TrancheBaremeDto ToDto(TrancheBareme t) =>
|
||||||
|
new(t.Id, t.Annee, t.PuissanceFiscale, t.KmMin, t.KmMax, t.Coefficient, t.Base);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Résultat d'un calcul de distance orchestré par <see cref="DistanceCalculatorService"/>.
|
||||||
|
/// Porte les coordonnées résolues des adresses de départ et d'arrivée, la distance
|
||||||
|
/// éventuellement calculée (routière via ORS ou orthodromique via Haversine), la
|
||||||
|
/// géométrie GeoJSON de l'itinéraire lorsqu'elle est disponible, et un éventuel
|
||||||
|
/// avertissement lorsque la distance n'a pas pu être calculée.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="DepartLatitude">Latitude résolue du point de départ (null si non résoluble).</param>
|
||||||
|
/// <param name="DepartLongitude">Longitude résolue du point de départ (null si non résoluble).</param>
|
||||||
|
/// <param name="ArriveeLatitude">Latitude résolue du point d'arrivée (null si non résoluble).</param>
|
||||||
|
/// <param name="ArriveeLongitude">Longitude résolue du point d'arrivée (null si non résoluble).</param>
|
||||||
|
/// <param name="DistanceKm">
|
||||||
|
/// Distance en kilomètres, arrondie à 2 décimales et bornée entre 0,01 et 99 999,99 km ;
|
||||||
|
/// <c>null</c> lorsque la distance n'a pas pu être calculée (aucune coordonnée résoluble).
|
||||||
|
/// </param>
|
||||||
|
/// <param name="GeometrieJson">
|
||||||
|
/// Géométrie GeoJSON de l'itinéraire routier (fournie par ORS) ; <c>null</c> en cas de
|
||||||
|
/// fallback Haversine ou d'absence de calcul.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="Avertissement">
|
||||||
|
/// Vrai lorsque la distance n'a pas pu être calculée faute de coordonnées résolubles ;
|
||||||
|
/// dans ce cas l'appelant stocke une distance nulle avec un avertissement.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="Message">Message d'avertissement associé, ou <c>null</c>.</param>
|
||||||
|
public record DistanceCalculationResult(
|
||||||
|
double? DepartLatitude,
|
||||||
|
double? DepartLongitude,
|
||||||
|
double? ArriveeLatitude,
|
||||||
|
double? ArriveeLongitude,
|
||||||
|
decimal? DistanceKm,
|
||||||
|
string? GeometrieJson,
|
||||||
|
bool Avertissement,
|
||||||
|
string? Message);
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implémentation de <see cref="IDistanceCalculator"/>.
|
||||||
|
///
|
||||||
|
/// Résout d'abord les coordonnées manquantes via BAN (priorité BAN sur ORS,
|
||||||
|
/// Property 16), puis calcule la distance routière via ORS. En cas de timeout ou
|
||||||
|
/// d'échec ORS, applique le fallback orthodromique (Haversine) sans erreur. Si les
|
||||||
|
/// coordonnées ne peuvent pas être résolues (BAN sans résultat / indisponible), retourne
|
||||||
|
/// une distance nulle assortie d'un avertissement, sans lever d'exception bloquante.
|
||||||
|
///
|
||||||
|
/// _Requirements: 2.1, 2.2, 2.3, 2.4, 2.6_
|
||||||
|
/// </summary>
|
||||||
|
public class DistanceCalculatorService : IDistanceCalculator
|
||||||
|
{
|
||||||
|
/// <summary>Distance minimale stockable (km).</summary>
|
||||||
|
private const decimal DistanceMinKm = 0.01m;
|
||||||
|
|
||||||
|
/// <summary>Distance maximale stockable (km).</summary>
|
||||||
|
private const decimal DistanceMaxKm = 99_999.99m;
|
||||||
|
|
||||||
|
private readonly IBANClient _banClient;
|
||||||
|
private readonly IORSClient _orsClient;
|
||||||
|
private readonly ILogger<DistanceCalculatorService> _logger;
|
||||||
|
|
||||||
|
public DistanceCalculatorService(
|
||||||
|
IBANClient banClient,
|
||||||
|
IORSClient orsClient,
|
||||||
|
ILogger<DistanceCalculatorService> logger)
|
||||||
|
{
|
||||||
|
_banClient = banClient;
|
||||||
|
_orsClient = orsClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<DistanceCalculationResult> CalculerAsync(
|
||||||
|
string adresseDepart,
|
||||||
|
string adresseArrivee,
|
||||||
|
double? departLatitude,
|
||||||
|
double? departLongitude,
|
||||||
|
double? arriveeLatitude,
|
||||||
|
double? arriveeLongitude,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Coordonnées de départ connues seulement si latitude ET longitude sont présentes.
|
||||||
|
double? depLat = departLatitude;
|
||||||
|
double? depLon = departLongitude;
|
||||||
|
double? arrLat = arriveeLatitude;
|
||||||
|
double? arrLon = arriveeLongitude;
|
||||||
|
|
||||||
|
// Résolution BAN AVANT tout appel ORS pour les coordonnées manquantes (Property 16).
|
||||||
|
if (!EstCoordonneeConnue(depLat, depLon))
|
||||||
|
{
|
||||||
|
var (lat, lon) = await GeocoderAsync(adresseDepart, cancellationToken);
|
||||||
|
depLat = lat;
|
||||||
|
depLon = lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EstCoordonneeConnue(arrLat, arrLon))
|
||||||
|
{
|
||||||
|
var (lat, lon) = await GeocoderAsync(adresseArrivee, cancellationToken);
|
||||||
|
arrLat = lat;
|
||||||
|
arrLon = lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si une coordonnée reste absente après géocodage BAN → distance nulle + avertissement.
|
||||||
|
if (!EstCoordonneeConnue(depLat, depLon) || !EstCoordonneeConnue(arrLat, arrLon))
|
||||||
|
{
|
||||||
|
return new DistanceCalculationResult(
|
||||||
|
DepartLatitude: depLat,
|
||||||
|
DepartLongitude: depLon,
|
||||||
|
ArriveeLatitude: arrLat,
|
||||||
|
ArriveeLongitude: arrLon,
|
||||||
|
DistanceKm: null,
|
||||||
|
GeometrieJson: null,
|
||||||
|
Avertissement: true,
|
||||||
|
Message: "Le trajet a été enregistré mais la distance n'a pas pu être "
|
||||||
|
+ "calculée faute de coordonnées géographiques.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// À ce stade, les quatre coordonnées sont non-null : jamais d'appel ORS avec des coordonnées nulles.
|
||||||
|
var lat1 = depLat!.Value;
|
||||||
|
var lon1 = depLon!.Value;
|
||||||
|
var lat2 = arrLat!.Value;
|
||||||
|
var lon2 = arrLon!.Value;
|
||||||
|
|
||||||
|
decimal distanceKm;
|
||||||
|
string? geometrieJson = null;
|
||||||
|
var avertissement = false;
|
||||||
|
string? message = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var route = await _orsClient.CalculateRouteAsync(lat1, lon1, lat2, lon2, cancellationToken);
|
||||||
|
distanceKm = route.DistanceKm;
|
||||||
|
geometrieJson = route.GeometrieJson;
|
||||||
|
}
|
||||||
|
catch (ORSTimeoutException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex,
|
||||||
|
"Timeout ORS : application du fallback Haversine pour le calcul de distance.");
|
||||||
|
distanceKm = HaversineKm(lat1, lon1, lat2, lon2);
|
||||||
|
}
|
||||||
|
catch (ORSException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex,
|
||||||
|
"Échec ORS : application du fallback Haversine pour le calcul de distance.");
|
||||||
|
distanceKm = HaversineKm(lat1, lon1, lat2, lon2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrondi 2 décimales + bornage [0,01 ; 99 999,99] km.
|
||||||
|
distanceKm = BornerDistance(Math.Round(distanceKm, 2, MidpointRounding.AwayFromZero));
|
||||||
|
|
||||||
|
return new DistanceCalculationResult(
|
||||||
|
DepartLatitude: lat1,
|
||||||
|
DepartLongitude: lon1,
|
||||||
|
ArriveeLatitude: lat2,
|
||||||
|
ArriveeLongitude: lon2,
|
||||||
|
DistanceKm: distanceKm,
|
||||||
|
GeometrieJson: geometrieJson,
|
||||||
|
Avertissement: avertissement,
|
||||||
|
Message: message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Géocode une adresse via BAN et retourne les coordonnées du meilleur (premier) résultat.
|
||||||
|
/// Retourne <c>(null, null)</c> si aucune adresse n'est trouvée ou si BAN est indisponible,
|
||||||
|
/// sans propager d'exception (échec BAN non bloquant).
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(double? Latitude, double? Longitude)> GeocoderAsync(
|
||||||
|
string adresse,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(adresse))
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resultats = await _banClient.RechercherAsync(adresse, cancellationToken);
|
||||||
|
if (resultats.Count == 0)
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meilleur / premier résultat (la BAN retourne les résultats par pertinence décroissante).
|
||||||
|
var meilleur = resultats[0];
|
||||||
|
return (meilleur.Latitude, meilleur.Longitude);
|
||||||
|
}
|
||||||
|
catch (BANIndisponibleException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex,
|
||||||
|
"BAN indisponible lors du géocodage de l'adresse manquante ; distance non calculable.");
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Vrai si latitude ET longitude sont toutes deux renseignées.</summary>
|
||||||
|
private static bool EstCoordonneeConnue(double? latitude, double? longitude)
|
||||||
|
=> latitude.HasValue && longitude.HasValue;
|
||||||
|
|
||||||
|
/// <summary>Borne une distance dans l'intervalle stockable [0,01 ; 99 999,99] km.</summary>
|
||||||
|
private static decimal BornerDistance(decimal distanceKm)
|
||||||
|
{
|
||||||
|
if (distanceKm < DistanceMinKm)
|
||||||
|
{
|
||||||
|
return DistanceMinKm;
|
||||||
|
}
|
||||||
|
|
||||||
|
return distanceKm > DistanceMaxKm ? DistanceMaxKm : distanceKm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcule la distance orthodromique (formule de Haversine) entre deux points, en
|
||||||
|
/// kilomètres, arrondie à 2 décimales. R = 6371 km (rayon moyen de la Terre).
|
||||||
|
/// </summary>
|
||||||
|
private static decimal HaversineKm(double lat1, double lon1, double lat2, double lon2)
|
||||||
|
{
|
||||||
|
const double R = 6371.0;
|
||||||
|
var dLat = (lat2 - lat1) * Math.PI / 180;
|
||||||
|
var dLon = (lon2 - lon1) * Math.PI / 180;
|
||||||
|
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2)
|
||||||
|
+ Math.Cos(lat1 * Math.PI / 180) * Math.Cos(lat2 * Math.PI / 180)
|
||||||
|
* Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
||||||
|
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||||||
|
return Math.Round((decimal)(R * c), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsque le texte d'adresse d'une Adresse_Nommee est absent, vide ou dépasse
|
||||||
|
/// 255 caractères (Exigence 15.8). Convertie en réponse HTTP 400 (Bad Request).
|
||||||
|
/// </summary>
|
||||||
|
public class AdresseNommeeInvalideException : Exception
|
||||||
|
{
|
||||||
|
public AdresseNommeeInvalideException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdresseNommeeInvalideException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'aucune Adresse_Nommee ne correspond à l'identifiant demandé
|
||||||
|
/// (modification, suppression). Convertie en réponse HTTP 404 (Not Found).
|
||||||
|
/// </summary>
|
||||||
|
public class AdresseNommeeNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public AdresseNommeeNotFoundException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdresseNommeeNotFoundException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsque l'API BAN (Base Adresses Nationale) est indisponible :
|
||||||
|
/// échec réseau, réponse HTTP non réussie ou dépassement du délai (timeout).
|
||||||
|
/// </summary>
|
||||||
|
public class BANIndisponibleException : Exception
|
||||||
|
{
|
||||||
|
public BANIndisponibleException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BANIndisponibleException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signale l'absence de barème configuré pour une (puissance fiscale, année) lors d'un calcul de montant.
|
||||||
|
/// Ce n'est pas une erreur bloquante : le calcul retourne un montant zéro accompagné d'un avertissement
|
||||||
|
/// (Exigence 13.4). Le service de calcul n'a donc pas à lever cette exception pour interrompre le calcul ;
|
||||||
|
/// elle est fournie pour les cas où un appelant souhaite explicitement signaler la non-configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class BaremeNonConfigureException : Exception
|
||||||
|
{
|
||||||
|
public BaremeNonConfigureException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaremeNonConfigureException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'une Date_Effet fournie pour un véhicule est postérieure à la date du jour.
|
||||||
|
/// Convertie en réponse HTTP 400 (Bad Request) — Exigence 10.11.
|
||||||
|
/// </summary>
|
||||||
|
public class DateEffetFutureException : Exception
|
||||||
|
{
|
||||||
|
public DateEffetFutureException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateEffetFutureException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'une Dénomination d'Adresse_Nommee, après normalisation (trim + minuscules +
|
||||||
|
/// suppression des accents), correspond à une Adresse_Nommee déjà enregistrée (Exigence 15.7).
|
||||||
|
/// Convertie en réponse HTTP 400 (Bad Request).
|
||||||
|
/// </summary>
|
||||||
|
public class DenominationDupliqueeException : Exception
|
||||||
|
{
|
||||||
|
public DenominationDupliqueeException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DenominationDupliqueeException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'une Dénomination d'Adresse_Nommee est absente, vide ou dépasse 100 caractères
|
||||||
|
/// (Exigence 15.6). Convertie en réponse HTTP 400 (Bad Request).
|
||||||
|
/// </summary>
|
||||||
|
public class DenominationInvalideException : Exception
|
||||||
|
{
|
||||||
|
public DenominationInvalideException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DenominationInvalideException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'un recalcul de la Distance échoue lors de la modification d'un Trajet.
|
||||||
|
/// Le service annule (rollback) la modification et conserve les données existantes du Trajet
|
||||||
|
/// sans modification, puis retourne cette erreur descriptive (Exigence 5.8).
|
||||||
|
/// Convertie en réponse HTTP 502 (Bad Gateway) / erreur descriptive par le contrôleur.
|
||||||
|
/// </summary>
|
||||||
|
public class DistanceRecalculException : Exception
|
||||||
|
{
|
||||||
|
public DistanceRecalculException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DistanceRecalculException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'un Montant_Regle soumis est négatif. Convertie en réponse HTTP 400
|
||||||
|
/// (Bad Request) — Exigence 14.4 : « Le montant réglé ne peut pas être négatif. »
|
||||||
|
/// </summary>
|
||||||
|
public class MontantRegleNegatifException : Exception
|
||||||
|
{
|
||||||
|
public MontantRegleNegatifException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MontantRegleNegatifException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsque la génération d'un Export_PDF échoue : erreur de rendu QuestPDF
|
||||||
|
/// ou dépassement du délai de 15 secondes (Exigence 7.5). Le controller la mappe
|
||||||
|
/// vers une réponse <c>503 Service Unavailable</c>.
|
||||||
|
/// </summary>
|
||||||
|
public class PdfGenerationException : Exception
|
||||||
|
{
|
||||||
|
public PdfGenerationException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PdfGenerationException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'un paramètre de résumé (mois ou année) est hors des plages autorisées :
|
||||||
|
/// mois ∈ [1, 12] et année ∈ [2000, 2100]. Convertie en réponse HTTP 400 (Bad Request)
|
||||||
|
/// par le controller — Exigence 3.6.
|
||||||
|
/// </summary>
|
||||||
|
public class ResumeParametreInvalideException : Exception
|
||||||
|
{
|
||||||
|
public ResumeParametreInvalideException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResumeParametreInvalideException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'une demande de modification ou de suppression cible un Trajet inexistant.
|
||||||
|
/// Convertie en réponse HTTP 404 (Not Found) ; l'opération est bloquée et aucun Résumé_Mensuel
|
||||||
|
/// n'est mis à jour (Exigences 5.6).
|
||||||
|
/// </summary>
|
||||||
|
public class TrajetNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public TrajetNotFoundException(int id)
|
||||||
|
: base($"Le trajet {id} n'existe pas.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrajetNotFoundException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrajetNotFoundException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'aucune tranche de barème ne correspond à l'identifiant demandé
|
||||||
|
/// (mise à jour ou suppression). Convertie en réponse HTTP 404 (Not Found).
|
||||||
|
/// </summary>
|
||||||
|
public class TrancheBaremeNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public TrancheBaremeNotFoundException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrancheBaremeNotFoundException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'une opération sur le barème produirait des tranches dont les intervalles
|
||||||
|
/// [KmMin, KmMax] se chevauchent pour une même (année, puissance fiscale).
|
||||||
|
/// Convertie en réponse HTTP 400 (Bad Request) — Exigence 13.2.
|
||||||
|
/// </summary>
|
||||||
|
public class TranchesBaremeChevauchantesException : Exception
|
||||||
|
{
|
||||||
|
public TranchesBaremeChevauchantesException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TranchesBaremeChevauchantesException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'aucun véhicule ne correspond à l'identifiant demandé
|
||||||
|
/// (résolution du véhicule courant, réaffectation, etc.).
|
||||||
|
/// Convertie en réponse HTTP 404 (Not Found).
|
||||||
|
/// </summary>
|
||||||
|
public class VehiculeNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public VehiculeNotFoundException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public VehiculeNotFoundException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestion du Cache_Adresses : recherche locale des adresses déjà connues, mise en cache
|
||||||
|
/// (upsert) des adresses géocodées et politique d'éviction LRU.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAdresseCacheService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Recherche locale des adresses en cache correspondant au fragment fourni.
|
||||||
|
/// Retourne une liste vide si le fragment fait moins de 3 caractères (Requirement 9.3).
|
||||||
|
/// Les suggestions sont ordonnées de la plus récemment utilisée à la plus ancienne.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Fragment de recherche (min. 3 caractères).</param>
|
||||||
|
/// <param name="limite">Nombre maximal de suggestions retournées (par défaut 5).</param>
|
||||||
|
Task<IReadOnlyList<AdresseCacheDto>> RechercherLocalAsync(
|
||||||
|
string query,
|
||||||
|
int limite = 5,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recherche l'entrée du cache correspondant exactement à l'adresse fournie (après normalisation).
|
||||||
|
/// Utilisé lors de la création d'un Trajet pour éviter un appel BAN (Requirement 9.2).
|
||||||
|
/// Met à jour la date de dernière utilisation de l'entrée trouvée.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Les coordonnées et la géométrie si l'adresse est en cache, sinon <c>null</c>.</returns>
|
||||||
|
Task<AdresseCacheEntreeDto?> ObtenirParAdresseAsync(
|
||||||
|
string adresse,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Insère l'adresse dans le cache si elle est absente, sinon met uniquement à jour sa date de
|
||||||
|
/// dernière utilisation sans modifier ses coordonnées (Requirements 9.1, 9.6). Applique ensuite
|
||||||
|
/// la politique d'éviction LRU (Requirement 9.5). L'ensemble s'exécute en transaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="adresse">Libellé de l'adresse (sera normalisé pour le matching).</param>
|
||||||
|
/// <param name="latitude">Latitude à utiliser si l'adresse est nouvelle.</param>
|
||||||
|
/// <param name="longitude">Longitude à utiliser si l'adresse est nouvelle.</param>
|
||||||
|
/// <param name="geometrieJson">Géométrie GeoJSON optionnelle si l'adresse est nouvelle.</param>
|
||||||
|
Task UpsertAdresseAsync(
|
||||||
|
string adresse,
|
||||||
|
double latitude,
|
||||||
|
double longitude,
|
||||||
|
string? geometrieJson = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestion du Carnet_Adresses : CRUD des Adresses_Nommées avec normalisation de la Dénomination
|
||||||
|
/// (unicité) et résolution des coordonnées via le Cache_Adresses ou l'API BAN (Exigence 15).
|
||||||
|
/// </summary>
|
||||||
|
public interface IAdresseNommeeService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Liste toutes les Adresses_Nommées du Carnet_Adresses, triées par Dénomination (Exigence 15.1).
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<AdresseNommeeDto>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crée une Adresse_Nommee : vérifie l'unicité de la Dénomination normalisée (15.6/15.7),
|
||||||
|
/// réutilise le Cache_Adresses si l'adresse y est déjà présente (15.10) ou géocode via BAN (15.11),
|
||||||
|
/// persiste avec ou sans coordonnées (15.5/15.12) et met en cache les coordonnées obtenues (15.9).
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Exceptions.DenominationDupliqueeException">Dénomination normalisée déjà utilisée (400).</exception>
|
||||||
|
Task<AdresseNommeeDto> CreateAsync(SaveAdresseNommeeDto dto, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modifie une Adresse_Nommee existante. Si le texte d'adresse a changé et n'est pas en cache,
|
||||||
|
/// re-géocode via BAN (15.11). Applique le contrôle d'unicité de la Dénomination (15.7).
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Exceptions.AdresseNommeeNotFoundException">Adresse_Nommee introuvable (404).</exception>
|
||||||
|
/// <exception cref="Exceptions.DenominationDupliqueeException">Dénomination normalisée déjà utilisée (400).</exception>
|
||||||
|
Task<AdresseNommeeDto> UpdateAsync(int id, SaveAdresseNommeeDto dto, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supprime une Adresse_Nommee du Carnet_Adresses. Les Trajets existants ne sont pas modifiés
|
||||||
|
/// (ils conservent leur texte d'adresse et leurs coordonnées) (Exigence 15.14).
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Exceptions.AdresseNommeeNotFoundException">Adresse_Nommee introuvable (404).</exception>
|
||||||
|
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client de l'API BAN (Base Adresses Nationale) pour l'autocomplétion
|
||||||
|
/// et le géocodage des adresses françaises.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBANClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Recherche des adresses correspondant à <paramref name="query"/> via l'endpoint
|
||||||
|
/// <c>/search/</c> de la BAN (limité à 5 résultats).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Texte de recherche (adresse partielle ou complète).</param>
|
||||||
|
/// <param name="cancellationToken">Jeton d'annulation.</param>
|
||||||
|
/// <returns>La liste des adresses trouvées, mappées en <see cref="BANAdresseDto"/>.</returns>
|
||||||
|
/// <exception cref="Exceptions.BANIndisponibleException">
|
||||||
|
/// Levée en cas d'échec réseau, de réponse non réussie ou de dépassement du délai.
|
||||||
|
/// </exception>
|
||||||
|
Task<IReadOnlyList<BANAdresseDto>> RechercherAsync(string query, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logique métier du barème kilométrique : CRUD des tranches, validation du non-chevauchement
|
||||||
|
/// par (année, puissance fiscale), sélection de tranche et calcul de montant.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBaremeService
|
||||||
|
{
|
||||||
|
/// <summary>Retourne toutes les tranches d'une année (toutes puissances fiscales confondues).</summary>
|
||||||
|
Task<IReadOnlyList<TrancheBaremeDto>> GetBaremeAsync(int annee, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ajoute une tranche. Rejette l'ajout s'il crée un chevauchement d'intervalle [KmMin, KmMax]
|
||||||
|
/// pour la même (année, puissance fiscale) via <see cref="Exceptions.TranchesBaremeChevauchantesException"/>.
|
||||||
|
/// </summary>
|
||||||
|
Task<TrancheBaremeDto> AddTrancheAsync(SaveTrancheBaremeDto dto, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Met à jour une tranche existante. Rejette la mise à jour si elle crée un chevauchement
|
||||||
|
/// (hors la tranche elle-même) via <see cref="Exceptions.TranchesBaremeChevauchantesException"/>.
|
||||||
|
/// Lève <see cref="Exceptions.TrancheBaremeNotFoundException"/> si la tranche n'existe pas.
|
||||||
|
/// </summary>
|
||||||
|
Task<TrancheBaremeDto> UpdateTrancheAsync(int id, SaveTrancheBaremeDto dto, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supprime une tranche. Lève <see cref="Exceptions.TrancheBaremeNotFoundException"/> si elle n'existe pas.
|
||||||
|
/// </summary>
|
||||||
|
Task DeleteTrancheAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sélectionne la tranche dont [KmMin, KmMax] contient <paramref name="totalKm"/> pour la
|
||||||
|
/// (année, puissance fiscale) donnée et calcule le montant `totalKm × Coefficient + Base`
|
||||||
|
/// (Property 24/27). Si aucune tranche n'existe pour cette combinaison, retourne un montant nul
|
||||||
|
/// et un signal « barème non configuré » sans lever d'erreur bloquante (Exigence 13.4).
|
||||||
|
/// </summary>
|
||||||
|
Task<BaremeCalculResult> CalculerMontantAsync(
|
||||||
|
int annee, int puissanceFiscale, decimal totalKm, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copie toutes les tranches de l'année la plus récente (strictement antérieure à
|
||||||
|
/// <paramref name="anneeCible"/>) qui possède un barème, vers <paramref name="anneeCible"/>.
|
||||||
|
/// Les tranches existantes de l'année cible sont d'abord supprimées (annule et remplace),
|
||||||
|
/// en transaction. Retourne le nombre de tranches copiées. Retourne 0 si aucun barème
|
||||||
|
/// antérieur n'existe (rien n'est modifié).
|
||||||
|
/// </summary>
|
||||||
|
Task<int> CopierDernierBaremeAsync(int anneeCible, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>Retourne les années (triées croissantes) pour lesquelles au moins une tranche de barème existe.</summary>
|
||||||
|
Task<IReadOnlyList<int>> GetAnneesAvecBaremeAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orchestre le calcul de la distance d'un trajet à partir des adresses de départ et
|
||||||
|
/// d'arrivée et de leurs coordonnées éventuelles.
|
||||||
|
///
|
||||||
|
/// Logique :
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Si les deux coordonnées sont connues → appel ORS ; en cas de timeout/échec ORS
|
||||||
|
/// → fallback orthodromique (Haversine), sans erreur pour l'utilisateur.</item>
|
||||||
|
/// <item>Si une coordonnée est absente → géocodage BAN de l'adresse manquante
|
||||||
|
/// (meilleur résultat) AVANT tout appel ORS ; ORS n'est jamais appelé avec des
|
||||||
|
/// coordonnées nulles (Property 16).</item>
|
||||||
|
/// <item>Si BAN ne peut pas résoudre (aucun résultat ou indisponible) → distance nulle
|
||||||
|
/// + avertissement, sans erreur bloquante, pour que l'appelant stocke la distance null.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public interface IDistanceCalculator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calcule la distance entre <paramref name="adresseDepart"/> et
|
||||||
|
/// <paramref name="adresseArrivee"/>, en résolvant au besoin les coordonnées manquantes
|
||||||
|
/// via BAN, puis en appelant ORS (avec fallback Haversine).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="adresseDepart">Texte de l'adresse de départ.</param>
|
||||||
|
/// <param name="adresseArrivee">Texte de l'adresse d'arrivée.</param>
|
||||||
|
/// <param name="departLatitude">Latitude de départ connue, ou <c>null</c>.</param>
|
||||||
|
/// <param name="departLongitude">Longitude de départ connue, ou <c>null</c>.</param>
|
||||||
|
/// <param name="arriveeLatitude">Latitude d'arrivée connue, ou <c>null</c>.</param>
|
||||||
|
/// <param name="arriveeLongitude">Longitude d'arrivée connue, ou <c>null</c>.</param>
|
||||||
|
/// <param name="cancellationToken">Jeton d'annulation.</param>
|
||||||
|
/// <returns>Un <see cref="DistanceCalculationResult"/> (jamais d'exception bloquante).</returns>
|
||||||
|
Task<DistanceCalculationResult> CalculerAsync(
|
||||||
|
string adresseDepart,
|
||||||
|
string adresseArrivee,
|
||||||
|
double? departLatitude,
|
||||||
|
double? departLongitude,
|
||||||
|
double? arriveeLatitude,
|
||||||
|
double? arriveeLongitude,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logique métier des Montant_Regle (montants effectivement versés, saisis par l'utilisateur)
|
||||||
|
/// et du Montant_Previsionnel (indication de versement à 80% du barème, purement informative).
|
||||||
|
/// </summary>
|
||||||
|
public interface IMontantRegleService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enregistre ou met à jour (upsert) le Montant_Regle pour un couple (année, mois, véhicule).
|
||||||
|
/// S'il existe déjà une ligne pour la même (Annee, Mois, VehiculeId), elle est mise à jour ;
|
||||||
|
/// sinon une nouvelle ligne est insérée (Property 26, Exigences 14.1/14.2/14.3).
|
||||||
|
/// Rejette un montant négatif via <see cref="Exceptions.MontantRegleNegatifException"/> (Exigence 14.4).
|
||||||
|
/// </summary>
|
||||||
|
Task<MontantRegleDto> UpsertMontantRegleAsync(
|
||||||
|
MontantRegleDto dto, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne les Montant_Regle saisis pour une (année, véhicule), triés par mois croissant.
|
||||||
|
/// Destiné au controller et au service de régularisation.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<MontantRegleDto>> GetMontantsReglesAsync(
|
||||||
|
int annee, int vehiculeId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcule le Montant_Previsionnel indicatif pour un (année, mois, véhicule) :
|
||||||
|
/// 80% du Bareme_Kilometrique plein appliqué au Total_Annuel_Partiel du véhicule
|
||||||
|
/// (somme des distances de l'année pour ce véhicule jusqu'au mois indiqué inclus),
|
||||||
|
/// arrondi à deux décimales (Property 23, Exigences 13.5/13.6).
|
||||||
|
/// Cette valeur est purement informative et n'entre jamais dans la régularisation.
|
||||||
|
/// </summary>
|
||||||
|
Task<MontantPrevisionnelDto> GetMontantPrevisionnelAsync(
|
||||||
|
int annee, int mois, int vehiculeId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client de l'API OpenRouteService (ORS) — calcul d'itinéraire routier uniquement.
|
||||||
|
/// ORS n'est jamais appelé pour du géocodage ni avec des coordonnées nulles :
|
||||||
|
/// les coordonnées de départ et d'arrivée doivent être connues (non-null).
|
||||||
|
/// </summary>
|
||||||
|
public interface IORSClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calcule l'itinéraire routier entre deux points géographiques connus via
|
||||||
|
/// <c>POST /v2/directions/driving-car</c> et retourne la distance (km) et la
|
||||||
|
/// géométrie GeoJSON de l'itinéraire.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lat1">Latitude du point de départ.</param>
|
||||||
|
/// <param name="lon1">Longitude du point de départ.</param>
|
||||||
|
/// <param name="lat2">Latitude du point d'arrivée.</param>
|
||||||
|
/// <param name="lon2">Longitude du point d'arrivée.</param>
|
||||||
|
/// <param name="cancellationToken">Jeton d'annulation.</param>
|
||||||
|
/// <returns>Un <see cref="RouteResult"/> (distance en km + géométrie GeoJSON).</returns>
|
||||||
|
/// <exception cref="ORSTimeoutException">
|
||||||
|
/// Levée si l'appel dépasse le délai imparti (voir <c>ORSOptions.TimeoutSeconds</c>),
|
||||||
|
/// afin que l'appelant applique le fallback orthodromique (Haversine).
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="ORSException">
|
||||||
|
/// Levée en cas d'échec général (erreur réseau, réponse invalide, corps illisible).
|
||||||
|
/// </exception>
|
||||||
|
Task<RouteResult> CalculateRouteAsync(
|
||||||
|
double lat1,
|
||||||
|
double lon1,
|
||||||
|
double lat2,
|
||||||
|
double lon2,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Génération des Export_PDF (QuestPDF). Compose les services métier existants
|
||||||
|
/// (résumé, profil, véhicule, barème, montants réglés) pour produire les documents.
|
||||||
|
/// La génération est enveloppée dans un délai de 15 secondes ; en cas de dépassement
|
||||||
|
/// ou d'erreur de rendu, une <see cref="Exceptions.PdfGenerationException"/> est levée
|
||||||
|
/// (Exigence 7.5) — le controller la mappe vers <c>503 Service Unavailable</c>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPdfService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Génère le rapport PDF mensuel d'un (année, mois) pour le véhicule indiqué (Exigences 7.1,
|
||||||
|
/// 7.4, 7.6, 7.8, 7.10, 7.11). L'en-tête contient l'identité de l'utilisateur, le véhicule
|
||||||
|
/// (plaque, puissance fiscale, coefficient et base de la tranche annuelle applicable), la
|
||||||
|
/// période, l'adresse de départ du profil, le total kilométrique du mois, le total kilométrique
|
||||||
|
/// annuel cumulé, le TOTAL IK du mois (km du mois × coefficient de la tranche annuelle) et le
|
||||||
|
/// Montant_Regle du mois. Le corps liste les trajets (date, motif/commentaire, départ → arrivée, km).
|
||||||
|
/// Le nom de fichier suggéré est <c>trajets_MM_AAAA.pdf</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Exceptions.PdfGenerationException">
|
||||||
|
/// Levée en cas de dépassement du délai de 15 s ou d'échec de rendu (Exigence 7.5).
|
||||||
|
/// </exception>
|
||||||
|
Task<PdfResult> GenererRapportMensuelAsync(
|
||||||
|
int annee, int mois, int vehiculeId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Génère la synthèse annuelle de régularisation d'une <paramref name="annee"/> pour le véhicule
|
||||||
|
/// indiqué (Exigences 7.2, 7.4, 7.6, 7.7, 7.8, 7.12). La page de synthèse contient le nom et le
|
||||||
|
/// prénom de l'utilisateur, l'année, l'adresse de domicile, le barème applicable de l'année (les
|
||||||
|
/// trois tranches coefficient/base par puissance fiscale du véhicule), le véhicule et sa puissance
|
||||||
|
/// fiscale, la tranche annuelle applicable ; puis pour chacun des douze mois : les kilomètres
|
||||||
|
/// parcourus, le Montant_Regle (frais payé) et le Frais_Reel_Mensuel (frais réel). Les totaux
|
||||||
|
/// (km annuel, total réglé, Montant_Reel, Regularisation) et la « formule réelle » sont indiqués.
|
||||||
|
/// Le nom de fichier suggéré est <c>trajets_AAAA.pdf</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Exceptions.PdfGenerationException">
|
||||||
|
/// Levée en cas de dépassement du délai de 15 s ou d'échec de rendu (Exigence 7.5).
|
||||||
|
/// </exception>
|
||||||
|
Task<PdfResult> GenererRapportAnnuelAsync(
|
||||||
|
int annee, int vehiculeId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Détermine les véhicules ayant des Trajets pour l'<paramref name="annee"/> et génère un rapport
|
||||||
|
/// annuel PDF distinct par véhicule (Exigence 7.9). Retourne un PDF par véhicule ayant des données,
|
||||||
|
/// trié par identifiant de véhicule. Retourne une liste vide si aucun trajet n'existe pour l'année.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Exceptions.PdfGenerationException">
|
||||||
|
/// Levée en cas de dépassement du délai de 15 s ou d'échec de rendu (Exigence 7.5).
|
||||||
|
/// </exception>
|
||||||
|
Task<IReadOnlyList<PdfResult>> GenererRapportsAnnuelsParVehiculeAsync(
|
||||||
|
int annee, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Détermine les véhicules ayant des Trajets pour le couple (<paramref name="annee"/>,
|
||||||
|
/// <paramref name="mois"/>) et génère un rapport mensuel PDF distinct par véhicule (Exigence 7.9).
|
||||||
|
/// Retourne un PDF par véhicule ayant des données, trié par identifiant de véhicule. Retourne une
|
||||||
|
/// liste vide si aucun trajet n'existe pour la période.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Exceptions.PdfGenerationException">
|
||||||
|
/// Levée en cas de dépassement du délai de 15 s ou d'échec de rendu (Exigence 7.5).
|
||||||
|
/// </exception>
|
||||||
|
Task<IReadOnlyList<PdfResult>> GenererRapportsMensuelsParVehiculeAsync(
|
||||||
|
int annee, int mois, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logique métier du profil utilisateur (singleton Id = 1) : lecture, sauvegarde avec
|
||||||
|
/// géocodage BAN de l'Adresse_Domicile et détermination de l'état de configuration (onboarding).
|
||||||
|
/// </summary>
|
||||||
|
public interface IProfilService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne le profil (singleton Id = 1). Crée l'enregistrement à la volée s'il est absent
|
||||||
|
/// afin de garantir qu'un profil vide soit toujours disponible à la lecture.
|
||||||
|
/// </summary>
|
||||||
|
Task<ProfilDto> GetProfilAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sauvegarde le profil. Géocode l'Adresse_Domicile via la BAN : en cas de succès, stocke les
|
||||||
|
/// coordonnées et positionne <c>DomicileGeocode = true</c> (avec mise en cache de l'adresse) ;
|
||||||
|
/// en cas d'échec BAN, persiste l'adresse textuelle sans coordonnées et <c>DomicileGeocode = false</c>
|
||||||
|
/// sans lever d'exception (Requirements 10.2, 10.7, 10.8).
|
||||||
|
/// </summary>
|
||||||
|
Task<ProfilDto> SaveProfilAsync(SaveProfilDto dto, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Détermine l'état de configuration du profil (onboarding) :
|
||||||
|
/// <c>EstConfigure = true</c> si et seulement si un Profil existe avec un Nom ET un Prénom non vides
|
||||||
|
/// (après trim) ET qu'au moins un Véhicule existe (Property 31, Requirements 16.1, 16.2).
|
||||||
|
/// </summary>
|
||||||
|
Task<EtatProfilDto> GetEtatAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logique métier de la régularisation annuelle (barème plein, 100%).
|
||||||
|
///
|
||||||
|
/// Contrairement au Montant_Previsionnel (80% indicatif, hors régularisation), la régularisation
|
||||||
|
/// applique le Bareme_Kilometrique plein au total kilométrique annuel COMPLET du véhicule :
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Montant_Reel = total_km_annuel × Coefficient + Base de la tranche déterminée par ce
|
||||||
|
/// total annuel (Exigence 14.5, Property 25/27).</item>
|
||||||
|
/// <item>Frais_Reel_Mensuel[m] = km_du_mois[m] × Coefficient de la tranche annuelle, de sorte que
|
||||||
|
/// la somme des douze Frais_Reel_Mensuel + la Base de la tranche = Montant_Reel (Exigence 14.6).</item>
|
||||||
|
/// <item>Regularisation = Montant_Reel − total des Montant_Regle de l'année (Exigence 14.7).</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public interface IRemboursementService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Construit la synthèse annuelle de régularisation pour un couple (année, véhicule) :
|
||||||
|
/// identité de la tranche annuelle applicable, 12 lignes mensuelles
|
||||||
|
/// (<see cref="LigneSyntheseMoisDto"/>) et totaux (km annuel, réglé, Montant_Reel, Regularisation).
|
||||||
|
/// </summary>
|
||||||
|
Task<RegularisationAnnuelleDto> GetRegularisationAnnuelleAsync(
|
||||||
|
int annee, int vehiculeId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logique métier des résumés kilométriques mensuels et annuels.
|
||||||
|
/// Les distances nulles (Trajet non calculé, Exigence 2.4) sont traitées comme 0 dans les totaux.
|
||||||
|
/// </summary>
|
||||||
|
public interface IResumeService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Construit le Résumé_Mensuel d'un mois : liste des Trajets triés par date croissante puis
|
||||||
|
/// par Position croissante, chacun avec sa Distance, et total kilométrique (somme des distances,
|
||||||
|
/// null → 0) arrondi à 2 décimales (Property 1 — Exigences 3.2, 3.3, 6.5).
|
||||||
|
/// Filtre par véhicule lorsque <paramref name="vehiculeId"/> est fourni ; sinon inclut tous les véhicules.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Exceptions.ResumeParametreInvalideException">
|
||||||
|
/// Levée si <paramref name="mois"/> ∉ [1, 12] ou <paramref name="annee"/> ∉ [2000, 2100] (Exigence 3.6).
|
||||||
|
/// </exception>
|
||||||
|
Task<ResumeMensuelDto> GetResumeMensuelAsync(
|
||||||
|
int annee, int mois, int? vehiculeId = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construit le Résumé_Annuel d'une année : toujours exactement 12 Résumés_Mensuels
|
||||||
|
/// (mois 1..12, les mois vides ayant un total de 0,00), et total annuel égal à la somme
|
||||||
|
/// des 12 totaux mensuels (Property 2, Property 7 — Exigences 4.2, 4.3, 4.4, 4.7, 6.6).
|
||||||
|
/// Filtre par véhicule lorsque <paramref name="vehiculeId"/> est fourni ; sinon inclut tous les véhicules.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Exceptions.ResumeParametreInvalideException">
|
||||||
|
/// Levée si <paramref name="annee"/> ∉ [2000, 2100] (Exigence 3.6).
|
||||||
|
/// </exception>
|
||||||
|
Task<ResumeAnnuelDto> GetResumeAnnuelAsync(
|
||||||
|
int annee, int? vehiculeId = null, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcul des totaux du Panneau_Totaux (jour, mois, Total_Annuel_Partiel) filtrés par véhicule
|
||||||
|
/// et fourniture des données de navigation temporelle (années, mois et jours disponibles).
|
||||||
|
/// </summary>
|
||||||
|
public interface ITotauxService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calcule, pour la <paramref name="date"/> sélectionnée et le véhicule indiqué, les totaux
|
||||||
|
/// kilométriques affichés dans le Panneau_Totaux (Exigences 12.7, 12.8, 12.19) :
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>TotalJour : somme des distances des Trajets de la journée.</description></item>
|
||||||
|
/// <item><description>TotalMois : somme des distances des Trajets du mois de la date.</description></item>
|
||||||
|
/// <item><description>TotalAnnuelPartiel : cumul de l'année jusqu'au mois de la date inclus,
|
||||||
|
/// à l'exclusion des mois strictement postérieurs (Property 17).</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// Les distances nulles sont comptées comme 0 et tous les totaux sont arrondis à 2 décimales.
|
||||||
|
/// Seuls les Trajets du véhicule sont additionnés (Property 21).
|
||||||
|
/// Si <paramref name="vehiculeId"/> vaut <c>null</c>, le Véhicule_Courant est utilisé
|
||||||
|
/// (via <see cref="IVehiculeService.GetVehiculeCourantAsync"/>).
|
||||||
|
/// </summary>
|
||||||
|
Task<TotauxDto> GetTotauxAsync(DateOnly date, int? vehiculeId = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne la liste des années comportant au moins un Trajet, à laquelle est ajoutée
|
||||||
|
/// l'année courante si elle n'y figure pas déjà (Exigence 12.15). Liste triée croissante.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<int>> GetAnneesDisponiblesAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne la liste des mois (1..12) de l'année <paramref name="annee"/> comportant au moins
|
||||||
|
/// un Trajet (Exigence 12.16). Liste triée croissante.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<int>> GetMoisDisponiblesAsync(int annee, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne la liste des jours du mois <paramref name="mois"/> de l'année <paramref name="annee"/>
|
||||||
|
/// comportant au moins un Trajet (Exigence 12.16). Liste triée croissante.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<int>> GetJoursDisponiblesAsync(int annee, int mois, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logique métier de gestion des Trajets. Cette interface couvre pour l'instant la
|
||||||
|
/// création (tâche 8.2) ; la modification, la suppression et le réordonnancement
|
||||||
|
/// (tâche 8.5) l'étendront ultérieurement.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITrajetService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Crée un Trajet à partir de <paramref name="dto"/> :
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>parse la date au format JJ/MM/AAAA (la validation de forme est faite par le validateur MVC) ;</item>
|
||||||
|
/// <item>associe le véhicule : <c>VehiculeId</c> fourni sinon le véhicule courant à la date
|
||||||
|
/// du trajet, en copiant sa <c>PuissanceFiscale</c> (dénormalisation, Exigence 10.14) ;</item>
|
||||||
|
/// <item>calcule la distance via <see cref="IDistanceCalculator"/> (BAN puis ORS, fallback Haversine) ;</item>
|
||||||
|
/// <item>met en cache les adresses résolues via <see cref="IAdresseCacheService"/> ;</item>
|
||||||
|
/// <item>attribue la position en fin de journée (<c>max(Position)+1.0</c>) ;</item>
|
||||||
|
/// <item>persiste le Trajet en transaction. Lorsque la distance n'a pas pu être calculée,
|
||||||
|
/// le Trajet est tout de même persisté avec une Distance nulle et un avertissement (Exigence 2.4).</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto">Données de création transmises par le contrôleur (déjà validées côté MVC).</param>
|
||||||
|
/// <param name="cancellationToken">Jeton d'annulation.</param>
|
||||||
|
/// <returns>Le <see cref="TrajetCreationResult"/> (trajet créé + éventuel avertissement).</returns>
|
||||||
|
/// <exception cref="Exceptions.VehiculeNotFoundException">
|
||||||
|
/// Si aucun véhicule n'est applicable à la date du trajet et qu'aucun <c>VehiculeId</c> n'est fourni.
|
||||||
|
/// </exception>
|
||||||
|
Task<TrajetCreationResult> CreateAsync(CreateTrajetDto dto, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modifie un Trajet existant à partir de <paramref name="dto"/> :
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>lève <see cref="Exceptions.TrajetNotFoundException"/> (404) si le Trajet est introuvable
|
||||||
|
/// (Exigence 5.6) ;</item>
|
||||||
|
/// <item>recalcule la Distance <b>uniquement</b> si au moins une adresse a changé (comparaison sur
|
||||||
|
/// le texte d'adresse normalisé) ou si le Trajet avait une Distance nulle et que les nouvelles
|
||||||
|
/// adresses deviennent résolubles (Exigences 5.2, 5.9, 5.10) ; sinon les autres champs (commentaire,
|
||||||
|
/// véhicule, date) sont mis à jour <b>sans</b> recalcul de distance (Exigence 5.3) ;</item>
|
||||||
|
/// <item>en cas d'échec du recalcul (coordonnées connues mais calcul impossible), annule la
|
||||||
|
/// modification (rollback) et lève <see cref="Exceptions.DistanceRecalculException"/> en préservant
|
||||||
|
/// les données existantes du Trajet (Exigence 5.8) ;</item>
|
||||||
|
/// <item>met en cache les adresses résolues (Exigences 6.7, 6.8) ;</item>
|
||||||
|
/// <item>persiste la modification en transaction.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Identifiant du Trajet à modifier.</param>
|
||||||
|
/// <param name="dto">Données de modification (déjà validées côté MVC).</param>
|
||||||
|
/// <param name="cancellationToken">Jeton d'annulation.</param>
|
||||||
|
/// <returns>Le <see cref="TrajetUpdateResult"/> (trajet mis à jour + éventuel avertissement).</returns>
|
||||||
|
/// <exception cref="Exceptions.TrajetNotFoundException">Si le Trajet n'existe pas.</exception>
|
||||||
|
/// <exception cref="Exceptions.DistanceRecalculException">Si le recalcul de la distance échoue.</exception>
|
||||||
|
/// <exception cref="Exceptions.VehiculeNotFoundException">
|
||||||
|
/// Si le <c>VehiculeId</c> fourni est introuvable, ou qu'aucun véhicule n'est applicable à la date.
|
||||||
|
/// </exception>
|
||||||
|
Task<TrajetUpdateResult> UpdateAsync(int id, UpdateTrajetDto dto, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supprime un Trajet existant de manière transactionnelle (Exigence 5.5).
|
||||||
|
/// Lève <see cref="Exceptions.TrajetNotFoundException"/> (404) si le Trajet est introuvable (Exigence 5.6).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Identifiant du Trajet à supprimer.</param>
|
||||||
|
/// <param name="cancellationToken">Jeton d'annulation.</param>
|
||||||
|
/// <exception cref="Exceptions.TrajetNotFoundException">Si le Trajet n'existe pas.</exception>
|
||||||
|
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applique un réordonnancement manuel des trajets d'une journée : met à jour uniquement la
|
||||||
|
/// <c>Position</c> des trajets fournis via <see cref="ITrajetRepository.ReordonnerAsync"/>,
|
||||||
|
/// sans altérer leurs autres champs (Exigence 11.9). Les identifiants inconnus sont ignorés.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto">Nouvelles positions des trajets à réordonner.</param>
|
||||||
|
/// <param name="cancellationToken">Jeton d'annulation.</param>
|
||||||
|
Task ReordonnerAsync(ReordonnerTrajetsDto dto, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recalcule la Distance (et la géométrie d'itinéraire mise en cache) de TOUS les trajets
|
||||||
|
/// via <see cref="IDistanceCalculator"/> (BAN + ORS, fallback Haversine), indépendamment de
|
||||||
|
/// tout changement d'adresse. Utile pour « réparer » des distances calculées en mode dégradé
|
||||||
|
/// (ex. clé ORS invalide → distances à vol d'oiseau) une fois ORS de nouveau opérationnel.
|
||||||
|
/// Chaque trajet est traité de façon indépendante : un échec sur l'un n'interrompt pas les autres.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Le nombre de trajets dont la distance a effectivement été recalculée.</returns>
|
||||||
|
Task<int> RecalculerToutesLesDistancesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logique métier de gestion des véhicules : historique, véhicule courant,
|
||||||
|
/// création (avec détection de réaffectation) et réaffectation des trajets.
|
||||||
|
/// </summary>
|
||||||
|
public interface IVehiculeService
|
||||||
|
{
|
||||||
|
/// <summary>Retourne l'historique complet des véhicules (plaque, puissance fiscale, date d'effet), trié par Date_Effet décroissante.</summary>
|
||||||
|
Task<IReadOnlyList<VehiculeDto>> GetHistoriqueAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne le Véhicule_Courant : Date_Effet la plus récente parmi celles <= aujourd'hui.
|
||||||
|
/// Lève <see cref="Exceptions.VehiculeNotFoundException"/> si aucun véhicule n'est applicable.
|
||||||
|
/// </summary>
|
||||||
|
Task<VehiculeDto> GetVehiculeCourantAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Résout le véhicule applicable à une date donnée (pour l'association d'un trajet) :
|
||||||
|
/// Date_Effet la plus récente <= <paramref name="date"/>.
|
||||||
|
/// Retourne <c>null</c> si aucun véhicule n'est applicable à cette date.
|
||||||
|
/// </summary>
|
||||||
|
Task<VehiculeDto?> GetVehiculeADateAsync(DateOnly date, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crée un véhicule, conserve l'historique et détecte les trajets à réaffecter
|
||||||
|
/// (date >= Date_Effet et puissance fiscale différente).
|
||||||
|
/// Lève <see cref="Exceptions.DateEffetFutureException"/> si la Date_Effet est postérieure à aujourd'hui.
|
||||||
|
/// </summary>
|
||||||
|
Task<VehiculeCreeDto> CreateAsync(CreateVehiculeDto dto, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applique (ou non) la réaffectation des trajets au véhicule indiqué.
|
||||||
|
/// Si <see cref="ReaffectationDto.Accepter"/> est vrai, met à jour VehiculeId + PuissanceFiscale
|
||||||
|
/// de tous les trajets dont la Date >= Date_Effet du véhicule ; sinon ne change rien.
|
||||||
|
/// Lève <see cref="Exceptions.VehiculeNotFoundException"/> si le véhicule n'existe pas.
|
||||||
|
/// </summary>
|
||||||
|
Task ReaffecterAsync(ReaffectationDto dto, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrajetKmTracker.Api.Data;
|
||||||
|
using TrajetKmTracker.Api.Models.DTOs;
|
||||||
|
using TrajetKmTracker.Api.Models.Entities;
|
||||||
|
using TrajetKmTracker.Api.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class MontantRegleService : IMontantRegleService
|
||||||
|
{
|
||||||
|
private const decimal TauxPrevisionnel = 0.80m;
|
||||||
|
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
private readonly IBaremeService _baremeService;
|
||||||
|
|
||||||
|
public MontantRegleService(AppDbContext context, IBaremeService baremeService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_baremeService = baremeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MontantRegleDto> UpsertMontantRegleAsync(
|
||||||
|
MontantRegleDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Défense en profondeur : le validateur FluentValidation impose déjà Montant >= 0,
|
||||||
|
// mais le service refuse aussi les montants négatifs (Exigence 14.4).
|
||||||
|
if (dto.Montant < 0m)
|
||||||
|
{
|
||||||
|
throw new MontantRegleNegatifException("Le montant réglé ne peut pas être négatif.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert par la clé métier (Annee, Mois, VehiculeId) — index unique
|
||||||
|
// UX_MontantsRegles_Periode_Vehicule garantit l'unicité (Property 26, Exigence 14.3).
|
||||||
|
var existant = await _context.MontantsRegles
|
||||||
|
.FirstOrDefaultAsync(
|
||||||
|
m => m.Annee == dto.Annee && m.Mois == dto.Mois && m.VehiculeId == dto.VehiculeId,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (existant is null)
|
||||||
|
{
|
||||||
|
var montantRegle = new MontantRegle
|
||||||
|
{
|
||||||
|
Annee = dto.Annee,
|
||||||
|
Mois = dto.Mois,
|
||||||
|
VehiculeId = dto.VehiculeId,
|
||||||
|
Montant = dto.Montant,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.MontantsRegles.Add(montantRegle);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ToDto(montantRegle);
|
||||||
|
}
|
||||||
|
|
||||||
|
existant.Montant = dto.Montant;
|
||||||
|
existant.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ToDto(existant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<MontantRegleDto>> GetMontantsReglesAsync(
|
||||||
|
int annee, int vehiculeId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var montants = await _context.MontantsRegles
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(m => m.Annee == annee && m.VehiculeId == vehiculeId)
|
||||||
|
.OrderBy(m => m.Mois)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return montants.Select(ToDto).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MontantPrevisionnelDto> GetMontantPrevisionnelAsync(
|
||||||
|
int annee, int mois, int vehiculeId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Puissance fiscale du véhicule concerné : détermine la tranche du barème.
|
||||||
|
var vehicule = await _context.Vehicules
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(v => v.Id == vehiculeId, cancellationToken)
|
||||||
|
?? throw new VehiculeNotFoundException($"Le véhicule d'identifiant {vehiculeId} est introuvable.");
|
||||||
|
|
||||||
|
// Total_Annuel_Partiel : somme des distances de l'année pour ce véhicule,
|
||||||
|
// jusqu'au mois sélectionné inclus (les mois postérieurs sont exclus).
|
||||||
|
// SQLite ne sait pas appliquer SUM sur une colonne decimal : on ramène les distances
|
||||||
|
// concernées et on somme côté client (null traité comme 0).
|
||||||
|
var distancesPartielles = await _context.Trajets
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.VehiculeId == vehiculeId
|
||||||
|
&& t.Date.Year == annee
|
||||||
|
&& t.Date.Month <= mois)
|
||||||
|
.Select(t => t.Distance)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var totalAnnuelPartiel = distancesPartielles.Sum(d => d ?? 0m);
|
||||||
|
|
||||||
|
// Barème plein appliqué au total partiel (sans minoration).
|
||||||
|
var bareme = await _baremeService.CalculerMontantAsync(
|
||||||
|
annee, vehicule.PuissanceFiscale, totalAnnuelPartiel, cancellationToken);
|
||||||
|
|
||||||
|
var montantBaremePlein = Math.Round(bareme.Montant, 2, MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
// Montant_Previsionnel = 80% du barème plein, arrondi à 2 décimales (indicatif — Exigences 13.5/13.6).
|
||||||
|
var montantPrevisionnel = Math.Round(bareme.Montant * TauxPrevisionnel, 2, MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
return new MontantPrevisionnelDto(
|
||||||
|
Annee: annee,
|
||||||
|
Mois: mois,
|
||||||
|
VehiculeId: vehiculeId,
|
||||||
|
TotalAnnuelPartielKm: Math.Round(totalAnnuelPartiel, 2, MidpointRounding.AwayFromZero),
|
||||||
|
MontantBaremePlein: montantBaremePlein,
|
||||||
|
MontantPrevisionnel: montantPrevisionnel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MontantRegleDto ToDto(MontantRegle m) =>
|
||||||
|
new(m.Annee, m.Mois, m.VehiculeId, m.Montant);
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client typé (HttpClient) de l'API OpenRouteService (ORS) pour le calcul
|
||||||
|
/// d'itinéraire routier. La <c>BaseAddress</c>, le <c>Timeout</c> et l'en-tête
|
||||||
|
/// de clé d'API sont configurés lors de l'enregistrement du client typé dans
|
||||||
|
/// <c>Program.cs</c> à partir de <c>ORSOptions</c>.
|
||||||
|
/// </summary>
|
||||||
|
public class ORSClient : IORSClient
|
||||||
|
{
|
||||||
|
// Point d'accès GeoJSON : retourne une FeatureCollection avec la géométrie
|
||||||
|
// de l'itinéraire au format GeoJSON et le résumé (distance en mètres).
|
||||||
|
private const string DirectionsGeoJsonPath = "v2/directions/driving-car/geojson";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public ORSClient(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<RouteResult> CalculateRouteAsync(
|
||||||
|
double lat1,
|
||||||
|
double lon1,
|
||||||
|
double lat2,
|
||||||
|
double lon2,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// ORS attend les coordonnées au format [longitude, latitude].
|
||||||
|
var body = new
|
||||||
|
{
|
||||||
|
coordinates = new[]
|
||||||
|
{
|
||||||
|
new[] { lon1, lat1 },
|
||||||
|
new[] { lon2, lat2 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpResponseMessage response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = await _httpClient.PostAsJsonAsync(DirectionsGeoJsonPath, body, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Le HttpClient annule via son Timeout : OperationCanceledException est levée
|
||||||
|
// sans que le jeton de l'appelant soit annulé -> il s'agit d'un dépassement de délai.
|
||||||
|
throw new ORSTimeoutException(
|
||||||
|
"L'appel à OpenRouteService a dépassé le délai imparti.", ex);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException ex)
|
||||||
|
{
|
||||||
|
throw new ORSTimeoutException(
|
||||||
|
"L'appel à OpenRouteService a dépassé le délai imparti.", ex);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
throw new ORSException(
|
||||||
|
"Échec de l'appel réseau à OpenRouteService.", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new ORSException(
|
||||||
|
$"OpenRouteService a retourné un statut non conforme : {(int)response.StatusCode}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
|
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
||||||
|
return ParseRouteResult(document.RootElement);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new ORSException(
|
||||||
|
"La réponse d'OpenRouteService n'a pas pu être analysée.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extrait la distance (mètres -> km) et la géométrie GeoJSON de la réponse ORS.
|
||||||
|
/// La réponse GeoJSON est une FeatureCollection dont la première feature contient
|
||||||
|
/// <c>properties.summary.distance</c> (mètres) et <c>geometry</c> (GeoJSON).
|
||||||
|
/// </summary>
|
||||||
|
private static RouteResult ParseRouteResult(JsonElement root)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty("features", out var features)
|
||||||
|
|| features.ValueKind != JsonValueKind.Array
|
||||||
|
|| features.GetArrayLength() == 0)
|
||||||
|
{
|
||||||
|
throw new ORSException(
|
||||||
|
"La réponse d'OpenRouteService ne contient aucun itinéraire.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var feature = features[0];
|
||||||
|
|
||||||
|
double distanceMeters;
|
||||||
|
if (feature.TryGetProperty("properties", out var properties)
|
||||||
|
&& properties.TryGetProperty("summary", out var summary)
|
||||||
|
&& summary.TryGetProperty("distance", out var distanceElement)
|
||||||
|
&& distanceElement.ValueKind == JsonValueKind.Number)
|
||||||
|
{
|
||||||
|
distanceMeters = distanceElement.GetDouble();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ORSException(
|
||||||
|
"La distance est absente de la réponse d'OpenRouteService.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion mètres -> km, arrondi à 2 décimales.
|
||||||
|
var distanceKm = Math.Round((decimal)(distanceMeters / 1000.0), 2, MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
string? geometrieJson = null;
|
||||||
|
if (feature.TryGetProperty("geometry", out var geometry)
|
||||||
|
&& geometry.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
geometrieJson = geometry.GetRawText();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteResult(distanceKm, geometrieJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée en cas d'échec général d'un appel à OpenRouteService (ORS) qui n'est pas
|
||||||
|
/// un dépassement de délai : erreur réseau, réponse HTTP non conforme, corps de
|
||||||
|
/// réponse illisible, etc. Distincte de <see cref="ORSTimeoutException"/> afin que
|
||||||
|
/// l'appelant puisse différencier un timeout (fallback Haversine) d'une défaillance
|
||||||
|
/// générale du service de calcul d'itinéraire.
|
||||||
|
/// </summary>
|
||||||
|
public class ORSException : Exception
|
||||||
|
{
|
||||||
|
public ORSException()
|
||||||
|
: base("Erreur lors de l'appel à OpenRouteService.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ORSException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ORSException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace TrajetKmTracker.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Levée lorsqu'un appel à OpenRouteService (ORS) dépasse le délai imparti
|
||||||
|
/// (voir <see cref="Configuration.ORSOptions.TimeoutSeconds"/>).
|
||||||
|
/// Le service appelant (DistanceCalculatorService) intercepte cette exception
|
||||||
|
/// pour appliquer le fallback orthodromique (Haversine) sans erreur bloquante
|
||||||
|
/// pour l'utilisateur. Voir Requirement 2.6.
|
||||||
|
/// </summary>
|
||||||
|
public class ORSTimeoutException : Exception
|
||||||
|
{
|
||||||
|
public ORSTimeoutException()
|
||||||
|
: base("L'appel à OpenRouteService a dépassé le délai imparti.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ORSTimeoutException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ORSTimeoutException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user