using MaBibli.Api.Services.Identite; using MaBibli.Api.Services.Series; using MaBibli.Shared.Dtos; namespace MaBibli.Api.Endpoints; public static class SeriesEndpoints { /// /// Sagas, cycles et séries : ce qui se lit dans un ordre, et ce qui manque pour le lire. /// /// /// ⚠️ Aucun point d'entrée n'accepte ni ne rend d'identité, à une exception près : /// la mise en envies. Les séries sont communes au foyer, comme le catalogue et les /// prêts — l'ordre de lecture d'une saga ne dépend pas de qui regarde. La liste d'envies, /// elle, reste personnelle, et c'est le serveur qui sait à qui il parle. /// /// La lecture est unique : GET /api/series rend tout l'arbre à plat. Le détail /// d'une série s'y lit sans second appel, ce qui donne un seul instantané hors-ligne à /// tenir à jour plutôt qu'un par série consultée. /// /// public static IEndpointRouteBuilder MapSeriesEndpoints(this IEndpointRouteBuilder routes) { var groupe = routes.MapGroup("/api/series").WithTags("Séries"); groupe.MapGet("/", async (IServiceSeries service, CancellationToken ct) => Results.Ok(await service.ListerAsync(ct))) .WithName("ListerSeries") .WithSummary("Toutes les séries, à plat, chacune avec ses tomes dans l'ordre de lecture.") .WithDescription( "Le client rebâtit l'arbre à partir de « serieParenteId ». Un tome sans " + "« livreId » est un tome qu'on ne possède pas : c'est ce que l'écran montre.") .Produces>(); groupe.MapPost("/", async ( EnregistrementSerie saisie, IServiceSeries service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) => { var resultat = await service.CreerAsync(saisie, utilisateurs.Obtenir().Identifiant, ct); return resultat.Erreur is not null ? Results.BadRequest(new { erreur = resultat.Erreur }) : Results.Created($"/api/series/{resultat.Serie!.Id}", resultat.Serie); }) .WithName("CreerSerie") .WithSummary("Crée une série. « AjoutePar » est déterminé par le serveur, et n'est qu'une trace.") .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest); groupe.MapPut("/{id:int}", async ( int id, EnregistrementSerie saisie, IServiceSeries service, CancellationToken ct) => Repondre(await service.ModifierAsync(id, saisie, ct))) .WithName("ModifierSerie") .WithSummary("Renomme la série, ou la range dans un cycle.") .Produces() .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound); groupe.MapDelete("/{id:int}", async (int id, IServiceSeries service, CancellationToken ct) => await service.SupprimerAsync(id, ct) ? Results.NoContent() : Results.NotFound()) .WithName("SupprimerSerie") .WithSummary("Supprime la série. Ses sous-séries remontent à la racine, elles ne sont pas détruites.") .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status404NotFound); groupe.MapPost("/{id:int}/elements", async ( int id, AjoutElementSerie saisie, IServiceSeries service, CancellationToken ct) => Repondre(await service.AjouterElementAsync(id, saisie, ct))) .WithName("AjouterElementSerie") .WithSummary("Ajoute un tome en fin de série : un livre du catalogue, ou un simple titre.") .Produces() .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound); groupe.MapPut("/{id:int}/ordre", async ( int id, OrdreElementsSerie ordre, IServiceSeries service, CancellationToken ct) => Repondre(await service.ReordonnerAsync(id, ordre.Ids, ct))) .WithName("ReordonnerSerie") .WithSummary("Fixe l'ordre de lecture — la liste entière des tomes, dans l'ordre voulu.") .Produces() .Produces(StatusCodes.Status404NotFound); groupe.MapPut("/{id:int}/sous-series/ordre", async ( int id, OrdreSeries ordre, IServiceSeries service, CancellationToken ct) => Repondre(await service.ReordonnerSousSeriesAsync(id, ordre.Ids, ct))) .WithName("ReordonnerSousSeries") .WithSummary("Fixe l'ordre de lecture des séries directement contenues dans un cycle.") .Produces() .Produces(StatusCodes.Status404NotFound); groupe.MapPut("/elements/{elementId:int}/livre", async ( int elementId, RattachementLivre rattachement, IServiceSeries service, CancellationToken ct) => Repondre(await service.RattacherLivreAsync(elementId, rattachement.LivreId, ct))) .WithName("RattacherLivreSerie") .WithSummary("Rattache un livre possédé à une place, ou l'en détache (« livreId » nul).") .WithDescription( "Détacher laisse un trou NOMMÉ : le titre du tome est conservé, il ne dépend " + "pas de l'exemplaire.") .Produces() .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound); groupe.MapDelete("/elements/{elementId:int}", async ( int elementId, IServiceSeries service, CancellationToken ct) => await service.RetirerElementAsync(elementId, ct) ? Results.NoContent() : Results.NotFound()) .WithName("RetirerElementSerie") .WithSummary("Retire une place de la série. Le livre, lui, reste au catalogue.") .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status404NotFound); groupe.MapPost("/elements/{elementId:int}/souhait", async ( int elementId, IServiceSeries service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) => { // ⚠️ Le seul endroit où une série (commune) touche à une liste d'envies // (personnelle). L'envie créée est celle de l'appelant, jamais celle du foyer. var resultat = await service.MettreEnEnviesAsync( elementId, utilisateurs.Obtenir().Identifiant, ct); return resultat.Erreur is not null ? Results.BadRequest(new { erreur = resultat.Erreur }) : Results.Ok(resultat.Souhait); }) .WithName("SouhaiterTomeManquant") .WithSummary("Met un tome manquant dans la liste d'envies de l'appelant.") .Produces() .Produces(StatusCodes.Status400BadRequest); return routes; } private static IResult Repondre(ResultatSerie resultat) { if (resultat.EstIntrouvable) { return Results.NotFound(); } return resultat.Erreur is not null ? Results.BadRequest(new { erreur = resultat.Erreur }) : Results.Ok(resultat.Serie); } } /// Charge utile du rattachement d'un livre à une place. null détache. public record RattachementLivre { public int? LivreId { get; set; } }