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; /// public class ProfilService : IProfilService { // Le profil est un singleton : une seule ligne dont l'identifiant est toujours 1. private const int ProfilId = 1; private readonly AppDbContext _context; private readonly IBANClient _banClient; private readonly IAdresseCacheService _adresseCacheService; private readonly ILogger _logger; public ProfilService( AppDbContext context, IBANClient banClient, IAdresseCacheService adresseCacheService, ILogger logger) { _context = context; _banClient = banClient; _adresseCacheService = adresseCacheService; _logger = logger; } /// public async Task GetProfilAsync(CancellationToken cancellationToken = default) { var profil = await GetOrCreateProfilAsync(cancellationToken); return ToDto(profil); } /// public async Task SaveProfilAsync(SaveProfilDto dto, CancellationToken cancellationToken = default) { var profil = await GetOrCreateProfilAsync(cancellationToken); profil.Nom = dto.Nom; profil.Prenom = dto.Prenom; profil.Matricule = dto.Matricule; profil.AdresseDomicile = dto.AdresseDomicile; profil.UpdatedAt = DateTime.UtcNow; // Géocodage BAN de l'Adresse_Domicile. En cas de succès on stocke les coordonnées et // DomicileGeocode = true ; en cas d'échec on persiste l'adresse textuelle sans coordonnées // et DomicileGeocode = false (aucune exception propagée) — Requirements 10.7, 10.8. await GeocoderDomicileAsync(profil, dto.AdresseDomicile, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return ToDto(profil); } /// public async Task GetEtatAsync(CancellationToken cancellationToken = default) { // On ne crée pas le profil ici : l'état doit refléter la base telle quelle (Property 31). var profil = await _context.Profil .AsNoTracking() .FirstOrDefaultAsync(p => p.Id == ProfilId, cancellationToken); var nomPrenomRenseignes = profil is not null && !string.IsNullOrWhiteSpace(profil.Nom) && !string.IsNullOrWhiteSpace(profil.Prenom); var auMoinsUnVehicule = await _context.Vehicules.AnyAsync(cancellationToken); return new EtatProfilDto(nomPrenomRenseignes && auMoinsUnVehicule); } /// /// Tente de résoudre les coordonnées de l'Adresse_Domicile via la BAN. Met à jour le profil /// (coordonnées + DomicileGeocode) et alimente le Cache_Adresses en cas de succès. Ne lève jamais : /// tout échec BAN est traité comme un géocodage non résolu. /// private async Task GeocoderDomicileAsync(Profil profil, string? adresseDomicile, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(adresseDomicile)) { // Pas d'adresse à géocoder : on repart d'un état non géocodé. profil.DomicileLatitude = null; profil.DomicileLongitude = null; profil.DomicileGeocode = false; return; } try { var resultats = await _banClient.RechercherAsync(adresseDomicile, cancellationToken); var meilleur = resultats.Count > 0 ? resultats[0] : null; if (meilleur is null) { // BAN a répondu mais aucune adresse ne correspond : adresse conservée sans coordonnées. profil.DomicileLatitude = null; profil.DomicileLongitude = null; profil.DomicileGeocode = false; return; } profil.DomicileLatitude = meilleur.Latitude; profil.DomicileLongitude = meilleur.Longitude; profil.DomicileGeocode = true; // Mise en cache de l'adresse géocodée pour réutilisation ultérieure (Requirement 9.1). await _adresseCacheService.UpsertAdresseAsync( adresseDomicile, meilleur.Latitude, meilleur.Longitude, geometrieJson: null, cancellationToken); } catch (BANIndisponibleException ex) { // BAN indisponible : l'adresse est conservée sans coordonnées, sans échec de la sauvegarde. _logger.LogWarning(ex, "Géocodage BAN de l'adresse de domicile indisponible ; l'adresse est conservée sans coordonnées."); profil.DomicileLatitude = null; profil.DomicileLongitude = null; profil.DomicileGeocode = false; } } /// /// Récupère le profil singleton (Id = 1) ou le crée à la volée s'il est absent. /// private async Task GetOrCreateProfilAsync(CancellationToken cancellationToken) { var profil = await _context.Profil.FirstOrDefaultAsync(p => p.Id == ProfilId, cancellationToken); if (profil is not null) { return profil; } profil = new Profil { Id = ProfilId }; await _context.Profil.AddAsync(profil, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return profil; } private static ProfilDto ToDto(Profil p) => new(p.Nom, p.Prenom, p.Matricule, p.AdresseDomicile, p.DomicileGeocode); }