using MaBibli.Api.Services.Catalogue; using MaBibli.Api.Services.Identite; using MaBibli.Shared.Catalogue; using MaBibli.Shared.Dtos; using MaBibli.Shared.Entites; namespace MaBibli.Api.Endpoints; public static class LivresEndpoints { /// /// CRUD du catalogue. /// /// /// Aucune lecture ne filtre sur AjoutePar : la bibliothèque est commune. /// Ce champ n'est même pas acceptable en paramètre de requête, précisément pour qu'on ne /// puisse pas s'en servir comme d'un cloisonnement par inadvertance. /// /// Le statut de lecture, lui, est personnel : il est toujours celui de l'appelant, /// déduit des en-têtes SSOwat. Aucun point d'entrée n'accepte un nom d'utilisateur — on ne /// consulte ni ne modifie la lecture de quelqu'un d'autre. /// /// public static IEndpointRouteBuilder MapLivresEndpoints(this IEndpointRouteBuilder routes) { var groupe = routes.MapGroup("/api/livres").WithTags("Livres"); groupe.MapGet("/", async ( string? recherche, Format? format, TypeDocument? typeDocument, Statut? statut, int? auteurId, bool? prete, IServiceCatalogue service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) => { var criteres = new CritereLivres { Recherche = recherche, Format = format, TypeDocument = typeDocument, Statut = statut, AuteurId = auteurId, Prete = prete, }; var utilisateur = utilisateurs.Obtenir().Identifiant; return Results.Ok(await service.ListerAsync(criteres, utilisateur, ct)); }) .WithName("ListerLivres") .WithSummary("Liste le catalogue, filtrable par format, type de document, statut, auteur et prêt en cours ; cherchable sur titre et auteurs.") .WithDescription( "Catalogue unique : physiques et numériques cohabitent, distingués par leur format. " + "La bibliothèque est commune — la liste n'est jamais restreinte à l'utilisateur courant. " + "Le filtre « statut » porte en revanche sur le statut de lecture de l'appelant, " + "qui lui est propre. La recherche ignore la casse et les accents.") .Produces>(); groupe.MapGet("/{id:int}", async ( int id, IServiceCatalogue service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) => { var livre = await service.ObtenirAsync(id, utilisateurs.Obtenir().Identifiant, ct); return livre is null ? Results.NotFound() : Results.Ok(livre); }) .WithName("ObtenirLivre") .Produces() .Produces(StatusCodes.Status404NotFound); groupe.MapPost("/", async ( EnregistrementLivre saisie, bool? confirmerDoublon, IServiceCatalogue service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) => { // AjoutePar vient du serveur, pas de la charge utile. var utilisateur = utilisateurs.Obtenir(); var resultat = await service.CreerAsync( saisie, utilisateur.Identifiant, confirmerDoublon ?? false, ct); if (resultat.Erreur is not null) { return Results.BadRequest(new { erreur = resultat.Erreur }); } // 409 et non 400 : la saisie est valide, c'est le catalogue qui contient déjà // quelque chose de semblable. Le même appel avec « confirmerDoublon » aboutira. return resultat.Doublons is { } doublons ? Results.Conflict(doublons) : Results.Created($"/api/livres/{resultat.Livre!.Id}", resultat.Livre); }) .WithName("CreerLivre") .WithSummary("Ajoute un livre. « AjoutePar » est déterminé par le serveur.") .WithDescription( "Répond 409 quand le catalogue contient déjà un livre de même ISBN, ou de même " + "titre et même auteur, en donnant les fiches en cause. Ce n'est pas un refus : " + "posséder deux exemplaires est légitime, et « confirmerDoublon=true » " + "enregistre la même saisie sans plus rien demander.") .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status409Conflict) .Produces(StatusCodes.Status400BadRequest); groupe.MapPut("/{id:int}", async ( int id, EnregistrementLivre saisie, IServiceCatalogue service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) => { var resultat = await service.ModifierAsync( id, saisie, utilisateurs.Obtenir().Identifiant, ct); if (resultat.EstIntrouvable) { return Results.NotFound(); } return resultat.Erreur is not null ? Results.BadRequest(new { erreur = resultat.Erreur }) : Results.Ok(resultat.Livre); }) .WithName("ModifierLivre") .WithSummary("Modifie un livre. « AjoutePar » et « DateAjout » restent inchangés ; le statut posé est celui de l'appelant.") .Produces() .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound); groupe.MapPatch("/{id:int}/statut", async ( int id, ChangementStatut changement, IServiceCatalogue service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) => { var livre = await service.ChangerStatutAsync( id, changement.Statut, utilisateurs.Obtenir().Identifiant, ct); return livre is null ? Results.NotFound() : Results.Ok(livre); }) .WithName("ChangerStatutLivre") .WithSummary("Change le seul statut de lecture de l'appelant — l'action la plus fréquente.") .WithDescription("Un statut nul remet le livre à « non commencé » : la ligne est supprimée.") .Produces() .Produces(StatusCodes.Status404NotFound); groupe.MapDelete("/{id:int}", async (int id, IServiceCatalogue service, CancellationToken ct) => { var supprime = await service.SupprimerAsync(id, ct); return supprime ? Results.NoContent() : Results.NotFound(); }) .WithName("SupprimerLivre") .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status404NotFound); return routes; } /// /// Consultation des auteurs et arbitrage des rapprochements ambigus. /// /// /// Les variantes sûres sont réunies automatiquement à l'écriture d'un livre et n'atteignent /// jamais ces points d'entrée. Ce qui arrive ici est ce que la machine refuse de /// trancher : un patronyme seul face à un nom complet, où seul l'utilisateur sait s'il /// s'agit d'une même personne. /// public static IEndpointRouteBuilder MapAuteursEndpoints(this IEndpointRouteBuilder routes) { var groupe = routes.MapGroup("/api/auteurs").WithTags("Auteurs"); groupe.MapGet("/", async (IServiceAuteurs service, CancellationToken ct) => Results.Ok(await service.ListerAsync(ct))) .WithName("ListerAuteurs") .WithSummary("Tous les auteurs du catalogue, avec leur nombre de livres.") .Produces>(); groupe.MapGet("/{id:int}", async (int id, IServiceAuteurs service, CancellationToken ct) => { var auteur = await service.ObtenirAsync(id, ct); return auteur is null ? Results.NotFound() : Results.Ok(auteur); }) .WithName("ObtenirAuteur") .Produces() .Produces(StatusCodes.Status404NotFound); groupe.MapPut("/{id:int}", async ( int id, RenommerAuteur demande, bool? confirmerFusion, IServiceAuteurs service, CancellationToken ct) => { var resultat = await service.RenommerAsync( id, demande.Nom, confirmerFusion ?? false, ct); if (resultat.Auteur is null && resultat.Collision is null) { return Results.NotFound(); } return resultat.Collision is not null ? Results.Conflict(resultat) : Results.Ok(resultat); }) .WithName("RenommerAuteur") .WithSummary("Renomme un auteur ou fusionne sa fiche avec une fiche existante.") .Produces() .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status409Conflict); groupe.MapGet("/rapprochements", async (IServiceAuteurs service, CancellationToken ct) => Results.Ok(await service.SuggestionsAsync(ct))) .WithName("ListerRapprochements") .WithSummary("Rapprochements ambigus proposés, refus déjà écartés.") .Produces>(); groupe.MapPost("/rapprochements/accepter", async ( DecisionRapprochement decision, IServiceAuteurs service, CancellationToken ct) => { var fusionne = await service.FusionnerAsync(decision.SourceId, decision.CibleId, ct); return fusionne ? Results.NoContent() : Results.NotFound(); }) .WithName("AccepterRapprochement") .WithSummary("Fusionne les deux auteurs : « source » disparaît au profit de « cible ».") .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status404NotFound); groupe.MapPost("/rapprochements/refuser", async ( DecisionRapprochement decision, IServiceAuteurs service, CancellationToken ct) => { var refuse = await service.RefuserAsync(decision.SourceId, decision.CibleId, ct); return refuse ? Results.NoContent() : Results.NotFound(); }) .WithName("RefuserRapprochement") .WithSummary("Mémorise que ces deux auteurs sont bien deux personnes : la suggestion ne reviendra plus.") .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status404NotFound); return routes; } /// Expose l'identité de la personne connectée, pour affichage côté client. public static IEndpointRouteBuilder MapIdentiteEndpoints(this IEndpointRouteBuilder routes) { routes.MapGet("/api/moi", (IFournisseurUtilisateur utilisateurs) => Results.Ok(utilisateurs.Obtenir())) .WithName("UtilisateurCourant") .WithTags("Identité") .WithSummary("Identité lue dans les en-têtes SSOwat, ou utilisateur simulé en développement.") .Produces(); return routes; } }