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