using MaBibli.Api.Data; using MaBibli.Shared.Dtos; using MaBibli.Shared.Entites; using MaBibli.Shared.Isbn; using MaBibli.Shared.Textes; using Microsoft.EntityFrameworkCore; namespace MaBibli.Api.Services.Souhaits; /// Issue d'un ajout d'envie : soit la ligne créée, soit un message pour l'utilisateur. public readonly record struct ResultatSouhait(SouhaitDto? Souhait, string? Erreur) { public static ResultatSouhait Ok(SouhaitDto souhait) => new(souhait, null); public static ResultatSouhait Invalide(string message) => new(null, message); } public interface IServiceSouhaits { Task> ListerAsync(string? utilisateur, CancellationToken ct = default); Task AjouterAsync( EnregistrementSouhait saisie, string? utilisateur, CancellationToken ct = default); Task SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default); } /// /// La liste d'envies, personnelle. /// /// /// ⚠️ Toute méthode filtre sur , sans exception. C'est la /// différence de fond avec ServiceCatalogue, dont les lectures ne filtrent jamais sur /// AjoutePar : le catalogue est commun, la liste d'envies ne l'est pas. Une envie qui /// fuiterait vers un autre membre du foyer gâcherait exactement ce que la fonctionnalité sert /// à préparer. /// /// Sans identité (ni en-tête SSOwat ni utilisateur simulé), il n'y a pas de liste : on ne rend /// rien et on n'écrit rien, plutôt que de rattacher des envies à un propriétaire inventé. /// /// public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits { public async Task> ListerAsync( string? utilisateur, CancellationToken ct = default) { if (utilisateur is null) { return []; } var souhaits = await db.LivresSouhaites .AsNoTracking() .Where(s => s.Utilisateur == utilisateur) .OrderBy(s => s.AuteurNormalise == string.Empty) .ThenBy(s => s.AuteurNormalise) .ThenBy(s => s.TitreNormalise) .ToListAsync(ct); return souhaits.Select(Projeter).ToList(); } public async Task AjouterAsync( EnregistrementSouhait saisie, string? utilisateur, CancellationToken ct = default) { if (utilisateur is null) { return ResultatSouhait.Invalide( "Impossible d'ajouter une envie sans savoir à qui elle appartient."); } if (string.IsNullOrWhiteSpace(saisie.Titre)) { return ResultatSouhait.Invalide("Le titre est obligatoire."); } // L'ISBN reste facultatif — on souhaite souvent une œuvre sans avoir choisi son édition — // mais s'il est saisi il doit tenir debout, comme pour un livre du catalogue. string? isbn = null; if (!string.IsNullOrWhiteSpace(saisie.Isbn)) { var normalise = IsbnUtils.Normaliser(saisie.Isbn); if (!IsbnUtils.EstValide(normalise)) { return ResultatSouhait.Invalide( $"« {saisie.Isbn} » n'est pas un ISBN valide. Laissez le champ vide si vous ne l'avez pas."); } isbn = normalise; } var souhait = new LivreSouhaite { Utilisateur = utilisateur, Titre = saisie.Titre, Auteur = saisie.Auteur, Editeur = Vide(saisie.Editeur), Annee = Vide(saisie.Annee), Isbn = isbn, CoverUrl = Vide(saisie.CoverUrl), Note = Vide(saisie.Note), DateAjout = DateTime.UtcNow, }; souhait.RecalculerFormes(); // Le doublon est refusé avec un message plutôt que laissé à l'index unique : l'écran de // bibliographie rend le double clic facile, et « UNIQUE constraint failed » ne veut rien // dire pour l'utilisateur. var deja = await db.LivresSouhaites.AnyAsync( s => s.Utilisateur == utilisateur && s.TitreNormalise == souhait.TitreNormalise && s.AuteurNormalise == souhait.AuteurNormalise, ct); if (deja) { return ResultatSouhait.Invalide($"« {souhait.Titre} » est déjà dans votre liste d'envies."); } db.LivresSouhaites.Add(souhait); await db.SaveChangesAsync(ct); return ResultatSouhait.Ok(Projeter(souhait)); } public async Task SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default) { if (utilisateur is null) { return false; } // Le filtre sur l'utilisateur fait partie de la CLÉ de recherche, pas d'une vérification // ultérieure : ainsi personne ne peut supprimer l'envie d'un autre en devinant son // identifiant. Une envie inexistante et une envie appartenant à un autre sont // indiscernables de l'extérieur, ce qui est bien le comportement voulu. var souhait = await db.LivresSouhaites .FirstOrDefaultAsync(s => s.Id == id && s.Utilisateur == utilisateur, ct); if (souhait is null) { return false; } db.LivresSouhaites.Remove(souhait); await db.SaveChangesAsync(ct); return true; } private static string? Vide(string? valeur) => string.IsNullOrWhiteSpace(valeur) ? null : valeur.Trim(); /// /// Projection vers le DTO. Sans le champ Utilisateur : il ne sort jamais de /// l'API, pour qu'aucun client ne puisse croire qu'il désigne de qui il parle. /// internal static SouhaitDto Projeter(LivreSouhaite souhait) => new() { Id = souhait.Id, Titre = souhait.Titre, Auteur = souhait.Auteur, Editeur = souhait.Editeur, Annee = souhait.Annee, Isbn = souhait.Isbn, CoverUrl = souhait.CoverUrl, Note = souhait.Note, DateAjout = souhait.DateAjout, }; }