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,
};
}