using MaBibli.Api.Data; using MaBibli.Shared.Catalogue; using MaBibli.Shared.Dtos; using MaBibli.Shared.Entites; using MaBibli.Shared.Isbn; using Microsoft.EntityFrameworkCore; namespace MaBibli.Api.Services.Catalogue; /// Issue d'une écriture : soit le livre, soit un message d'erreur destiné à l'utilisateur. public readonly record struct ResultatEcriture(LivreDto? Livre, string? Erreur) { public static ResultatEcriture Ok(LivreDto livre) => new(livre, null); public static ResultatEcriture Invalide(string message) => new(null, message); /// Ni livre ni erreur : la ressource demandée n'existe pas. public static readonly ResultatEcriture Introuvable = new(null, null); public bool EstIntrouvable => Livre is null && Erreur is null; } public interface IServiceCatalogue { Task> ListerAsync(CritereLivres criteres, CancellationToken ct = default); Task ObtenirAsync(int id, CancellationToken ct = default); Task CreerAsync(EnregistrementLivre saisie, string? ajoutePar, CancellationToken ct = default); Task ModifierAsync(int id, EnregistrementLivre saisie, CancellationToken ct = default); Task ChangerStatutAsync(int id, Statut statut, CancellationToken ct = default); Task SupprimerAsync(int id, CancellationToken ct = default); } /// /// CRUD du catalogue de livres — physiques et numériques confondus, distingués par /// . Il n'y a qu'un seul catalogue. /// public sealed class ServiceCatalogue(MaBibliDbContext db) : IServiceCatalogue { public async Task> ListerAsync( CritereLivres criteres, CancellationToken ct = default) { // AsNoTracking : lecture pure. Et surtout : AUCUN filtre sur AjoutePar, // la bibliothèque est commune à tout le foyer (CLAUDE.md). var requete = FiltreLivres.Appliquer(db.Livres.AsNoTracking(), criteres); var livres = await requete.ToListAsync(ct); return livres.Select(Projeter).ToList(); } public async Task ObtenirAsync(int id, CancellationToken ct = default) { var livre = await db.Livres.AsNoTracking().FirstOrDefaultAsync(l => l.Id == id, ct); return livre is null ? null : Projeter(livre); } public async Task CreerAsync( EnregistrementLivre saisie, string? ajoutePar, CancellationToken ct = default) { if (Valider(saisie, out var isbn, out var erreur) is false) { return ResultatEcriture.Invalide(erreur!); } var livre = new Livre { Isbn = isbn, Titre = saisie.Titre.Trim(), Auteur = Vide(saisie.Auteur), Editeur = Vide(saisie.Editeur), Format = saisie.Format, Statut = saisie.Statut, CoverUrl = Vide(saisie.CoverUrl), DateAjout = DateTime.UtcNow, // Renseigné par le serveur à partir de l'utilisateur authentifié, jamais par le client. AjoutePar = ajoutePar, }; db.Livres.Add(livre); await db.SaveChangesAsync(ct); return ResultatEcriture.Ok(Projeter(livre)); } public async Task ModifierAsync( int id, EnregistrementLivre saisie, CancellationToken ct = default) { var livre = await db.Livres.FirstOrDefaultAsync(l => l.Id == id, ct); if (livre is null) { return ResultatEcriture.Introuvable; } if (Valider(saisie, out var isbn, out var erreur) is false) { return ResultatEcriture.Invalide(erreur!); } livre.Isbn = isbn; livre.Titre = saisie.Titre.Trim(); livre.Auteur = Vide(saisie.Auteur); livre.Editeur = Vide(saisie.Editeur); livre.Format = saisie.Format; livre.Statut = saisie.Statut; livre.CoverUrl = Vide(saisie.CoverUrl); // DateAjout et AjoutePar ne sont jamais réécrits : ce sont des traces de la saisie // d'origine, pas des champs éditables. await db.SaveChangesAsync(ct); return ResultatEcriture.Ok(Projeter(livre)); } public async Task ChangerStatutAsync(int id, Statut statut, CancellationToken ct = default) { var livre = await db.Livres.FirstOrDefaultAsync(l => l.Id == id, ct); if (livre is null) { return null; } livre.Statut = statut; await db.SaveChangesAsync(ct); return Projeter(livre); } public async Task SupprimerAsync(int id, CancellationToken ct = default) { var livre = await db.Livres.FirstOrDefaultAsync(l => l.Id == id, ct); if (livre is null) { return false; } db.Livres.Remove(livre); await db.SaveChangesAsync(ct); return true; } /// /// Contrôles minimaux : un titre, et un ISBN qui tienne debout s'il est fourni. /// /// /// L'ISBN est facultatif (livres anciens, tirages sans ISBN) mais, s'il est saisi, il doit /// être valide : un ISBN faux est pire que pas d'ISBN, il empêcherait tout rapprochement /// futur avec les catalogues. /// private static bool Valider(EnregistrementLivre saisie, out string? isbn, out string? erreur) { isbn = null; erreur = null; if (string.IsNullOrWhiteSpace(saisie.Titre)) { erreur = "Le titre est obligatoire."; return false; } if (!string.IsNullOrWhiteSpace(saisie.Isbn)) { var normalise = IsbnUtils.Normaliser(saisie.Isbn); if (!IsbnUtils.EstValide(normalise)) { erreur = $"« {saisie.Isbn} » n'est pas un ISBN valide. Laissez le champ vide si le livre n'en a pas."; return false; } isbn = normalise; } return true; } private static string? Vide(string? valeur) => string.IsNullOrWhiteSpace(valeur) ? null : valeur.Trim(); private static LivreDto Projeter(Livre livre) => new() { Id = livre.Id, Isbn = livre.Isbn, Titre = livre.Titre, Auteur = livre.Auteur, Editeur = livre.Editeur, Format = livre.Format, Statut = livre.Statut, CoverUrl = livre.CoverUrl, DateAjout = livre.DateAjout, AjoutePar = livre.AjoutePar, }; }