using MaBibli.Api.Data; using MaBibli.Shared.Dtos; using MaBibli.Shared.Entites; using Microsoft.EntityFrameworkCore; namespace MaBibli.Api.Services.Souhaits; /// Issue d'une écriture d'envie de revue : la ligne, un message, ou rien du tout. public readonly record struct ResultatRevueSouhaitee( RevueSouhaiteeDto? Envie, string? Erreur, bool NExistePas = false) { public static readonly ResultatRevueSouhaitee Introuvable = new(null, null, true); public static ResultatRevueSouhaitee Ok(RevueSouhaiteeDto envie) => new(envie, null); public static ResultatRevueSouhaitee Invalide(string message) => new(null, message); } public interface IServiceRevuesSouhaitees { Task> ListerAsync( string? utilisateur, CancellationToken ct = default); Task AjouterAsync( EnregistrementRevueSouhaitee saisie, string? utilisateur, CancellationToken ct = default); Task ModifierAsync( int id, EnregistrementRevueSouhaitee saisie, string? utilisateur, CancellationToken ct = default); Task SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default); } /// /// Les envies de revues et de numéros, personnelles. /// /// /// Mêmes règles que , sans exception : toute méthode filtre sur /// l'appelant, aucun point d'entrée n'accepte de nom d'utilisateur, et l'envie d'un autre est /// simplement introuvable. /// /// ⚠️ Il n'y a pas de rapprochement avec les revues possédées, contrairement aux envies /// de livres. Ce n'est pas un oubli : « je possède la revue mais pas ce numéro » et « je possède /// ce numéro » demandent deux réponses différentes, et un signalement approximatif sur une liste /// d'envies coûte plus qu'il ne rapporte. /// /// public sealed class ServiceRevuesSouhaitees(MaBibliDbContext db) : IServiceRevuesSouhaitees { public async Task> ListerAsync( string? utilisateur, CancellationToken ct = default) { if (utilisateur is null) { return []; } var envies = await db.RevuesSouhaitees .AsNoTracking() .Where(e => e.Utilisateur == utilisateur) .OrderBy(e => e.Rang) .ThenBy(e => e.Id) .ToListAsync(ct); return envies.Select(Projeter).ToList(); } public async Task AjouterAsync( EnregistrementRevueSouhaitee saisie, string? utilisateur, CancellationToken ct = default) { if (utilisateur is null) { return ResultatRevueSouhaitee.Invalide( "Impossible d'ajouter une envie sans savoir à qui elle appartient."); } if (string.IsNullOrWhiteSpace(saisie.Titre)) { return ResultatRevueSouhaitee.Invalide("Le titre de la revue est obligatoire."); } var envie = new RevueSouhaitee { Utilisateur = utilisateur, Titre = saisie.Titre, Numero = saisie.Numero, Issn = saisie.Issn, Note = Vide(saisie.Note), DateAjout = DateTime.UtcNow, }; // ⚠️ Canonise l'ISSN au passage : « 24666718 » devient « 2466-6718 », la seule forme // rangée en base et la seule que la BnF interroge. envie.RecalculerFormes(); if (await ExisteDejaAsync(envie, null, ct)) { return ResultatRevueSouhaitee.Invalide(Deja(envie)); } // En fin de section, comme une envie de livre : on vient de la noter, on n'a pas dit // qu'on la voulait plus que les autres. var dernierRang = await db.RevuesSouhaitees .Where(e => e.Utilisateur == utilisateur) .Select(e => (int?)e.Rang) .MaxAsync(ct); envie.Rang = (dernierRang ?? -1) + 1; db.RevuesSouhaitees.Add(envie); await db.SaveChangesAsync(ct); return ResultatRevueSouhaitee.Ok(Projeter(envie)); } public async Task ModifierAsync( int id, EnregistrementRevueSouhaitee saisie, string? utilisateur, CancellationToken ct = default) { if (utilisateur is null) { return ResultatRevueSouhaitee.Introuvable; } // Le filtre fait partie de la clé de recherche : l'envie d'un autre n'est pas refusée, // elle n'est pas trouvée. var envie = await db.RevuesSouhaitees .FirstOrDefaultAsync(e => e.Id == id && e.Utilisateur == utilisateur, ct); if (envie is null) { return ResultatRevueSouhaitee.Introuvable; } if (string.IsNullOrWhiteSpace(saisie.Titre)) { return ResultatRevueSouhaitee.Invalide("Le titre de la revue est obligatoire."); } envie.Titre = saisie.Titre; envie.Numero = saisie.Numero; envie.Issn = saisie.Issn; envie.Note = Vide(saisie.Note); envie.RecalculerFormes(); if (await ExisteDejaAsync(envie, envie.Id, ct)) { return ResultatRevueSouhaitee.Invalide(Deja(envie)); } // Rang inchangé : corriger un numéro ne dit rien de l'ordre voulu. await db.SaveChangesAsync(ct); return ResultatRevueSouhaitee.Ok(Projeter(envie)); } public async Task SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default) { if (utilisateur is null) { return false; } var envie = await db.RevuesSouhaitees .FirstOrDefaultAsync(e => e.Id == id && e.Utilisateur == utilisateur, ct); if (envie is null) { return false; } db.RevuesSouhaitees.Remove(envie); await db.SaveChangesAsync(ct); return true; } /// /// Le doublon est refusé avec un message, jamais laissé à l'index unique. /// /// /// ⚠️ L'entité est déjà rattachée au contexte lors d'une modification : d'où l'exclusion /// explicite de sa propre ligne, sans quoi toute édition se dirait doublon d'elle-même. /// private Task ExisteDejaAsync(RevueSouhaitee envie, int? saufId, CancellationToken ct) => db.RevuesSouhaitees.AnyAsync( e => e.Utilisateur == envie.Utilisateur && e.TitreNormalise == envie.TitreNormalise && e.NumeroNormalise == envie.NumeroNormalise && (saufId == null || e.Id != saufId), ct); private static string Deja(RevueSouhaitee envie) => envie.Numero is null ? $"« {envie.Titre} » est déjà dans votre liste d'envies." : $"« {envie.Titre} n° {envie.Numero} » est déjà dans votre liste d'envies."; private static string? Vide(string? valeur) => string.IsNullOrWhiteSpace(valeur) ? null : valeur.Trim(); /// Projection vers le DTO, sans le champ Utilisateur. private static RevueSouhaiteeDto Projeter(RevueSouhaitee envie) => new() { Id = envie.Id, Titre = envie.Titre, Numero = envie.Numero, Issn = envie.Issn, Note = envie.Note, DateAjout = envie.DateAjout, }; }