Création client (POST /api/travels/{id}/comments) : serveur force IsApproved = false + CustomerId = customer.Id (anti-bypass, cf. commit 2946f14d).
Moyenne d'étoiles d'un voyage (Travel.CommentsAverageRating, Travel.CommentsCount) ne compte que les IsApproved && Rating.HasValue.
Réponse Buchard — règles :
Author = "Buchard Voyages", IsApproved = true, Rating = null, CustomerId = null, TravelId hérité du parent — tout est forcé côté service, jamais lu de l'input.
Single-level : refus si parent.ParentCommentId != null.
Un seul reply par parent : refus si parent.Replies.Any().
Texte non vide.
Cascade delete : DeleteComment / BulkDeleteComments suppriment manuellement les Replies avant le parent (la FK est Restrict côté SQL — SQL Server ne supporte pas Cascade sur self-ref).
Listing modération (SearchComments) ne renvoie que les racines (ParentCommentId == null) avec Include(Replies). La projection DTO aplatit la (seule possible) réponse.
Badge "en attente" : GetPendingCommentsCount filtre ParentCommentId == null && !IsApproved (défense en profondeur).
API publique (/api/comments, /api/travels/{id}/comments) : opère uniquement sur les avis clients racine. Le DTO TravelCommentDton'expose pasParentCommentId ni les Replies à ce stade — les réponses Buchard sont back-office only (choix produit explicite, à reconsidérer si on veut les afficher sur le site/app).
Back-office (Pages/Comments) : modération + réponses, gated [Authorize(Roles = $"{Roles.ADMINISTRATOR},{Roles.PRODUCTION}")]. Les deux rôles ont strictement le même accès sur toutes les actions (voir, approuver, désapprouver, éditer, répondre, supprimer, bulk, badge pending). Dans _Layout.cshtml, l'item "Commentaires" est placé en top-level juste après "Tableau de bord" (visible Admin + Production), volontairement hors de la section "Administration" qui reste Admin-only (Utilisateurs, Paramètres).
Si !hasReply : Répondre (ouvre kt_comments_modal_reply → POST /Comments/Reply).
Si hasReply : Modifier la réponse (réutilise kt_comments_modal_edit via data-edit-target="reply", prefill replyComment, target = replyId, va sur POST /Comments/Edit) + Supprimer la réponse (POST /Comments/DeleteReply).
Modale « Voir » : affiche le commentaire client + bloc « Réponse Buchard Voyages » en dessous si hasReply.
Bulk actions inchangées : approve / disapprove / delete (cascade sur les replies).
La FK self-ref est en Restrict — toute logique de suppression doit cascader manuellement. Ne pas changer ça côté EF sans tester (SQL Server refuse cascade self-ref, c'est intentionnel).
Le Author libre n'est pas garanti unique ni typé — "Buchard Voyages" n'est PAS un marqueur fiable pour différencier reply ≠ commentaire racine. Toujours utiliser ParentCommentId comme source de vérité.
L'AutoMapper TravelCommentDto ↔ TravelComment est un ReverseMap() brut — toute nouvelle propriété sur le DTO sera automatiquement mappée. Si on ajoute des champs sensibles côté DTO un jour, refaire le ReverseMap manuellement.
Travel.CommentsAverageRating ignore les replies naturellement (elles ont Rating = null), mais si on les expose un jour côté API/front, vérifier qu'on n'inclut pas par accident Replies dans le compte.
Migrations EF — drift Identity : le tooling dotnet ef 6.0.9 vs runtime 8.0.7 génère systématiquement des AlterColumn parasites sur AspNetUserTokens/AspNetUserLogins. Convention du projet : les supprimer manuellement de la migration générée (cf. 20260518085341_AddTravelCommentParent.cs).
Module — Commentaires & modération
Rôle
Pages/Comments) : approuver, désapprouver, éditer le texte, supprimer.Emplacement
Domain/Entities/TravelComment.csApplicationDbContext.TravelComments+ self-ref FK config (cf.OnModelCreating)*Comment*/AddAdminReplysurTravelService(interfaceITravelService)POST /api/travels/{id}/commentssurAreas/Api/TravelControllerAreas/Api/CommentController— GET un, GET mine, PUT, DELETEPages/Comments/Index.cshtml{.cs}+_ListActions.cshtmlApplication/Dtos/Api/Travel/TravelCommentDto.csApplication/Dtos/SearchResult/TravelCommentSearchResultDto.csDomain/Models/Search/TravelCommentSearchModel.csEntités principales
TravelCommentId,TravelId(FK),CustomerId?(FK, nullable pour les avis legacy sans auteur lié)Author(string, libre),Comment,Rating?(1-5)IsApproved— modérationParentCommentId?— self-ref pour la réponse Buchard (null = commentaire racine client)Travel,Customer,ParentComment,Replies(collection)TravelComment.BuchardReplyAuthorName = "Buchard Voyages"Règles métier spécifiques
POST /api/travels/{id}/comments) : serveur forceIsApproved = false+CustomerId = customer.Id(anti-bypass, cf. commit 2946f14d).Travel.CommentsAverageRating,Travel.CommentsCount) ne compte que lesIsApproved && Rating.HasValue.Author = "Buchard Voyages",IsApproved = true,Rating = null,CustomerId = null,TravelIdhérité du parent — tout est forcé côté service, jamais lu de l'input.parent.ParentCommentId != null.parent.Replies.Any().DeleteComment/BulkDeleteCommentssuppriment manuellement lesRepliesavant le parent (la FK estRestrictcôté SQL — SQL Server ne supporte pasCascadesur self-ref).SearchComments) ne renvoie que les racines (ParentCommentId == null) avecInclude(Replies). La projection DTO aplatit la (seule possible) réponse.GetPendingCommentsCountfiltreParentCommentId == null && !IsApproved(défense en profondeur).API publique vs back-office
/api/comments,/api/travels/{id}/comments) : opère uniquement sur les avis clients racine. Le DTOTravelCommentDton'expose pasParentCommentIdni lesRepliesà ce stade — les réponses Buchard sont back-office only (choix produit explicite, à reconsidérer si on veut les afficher sur le site/app).Pages/Comments) : modération + réponses, gated[Authorize(Roles = $"{Roles.ADMINISTRATOR},{Roles.PRODUCTION}")]. Les deux rôles ont strictement le même accès sur toutes les actions (voir, approuver, désapprouver, éditer, répondre, supprimer, bulk, badge pending). Dans_Layout.cshtml, l'item "Commentaires" est placé en top-level juste après "Tableau de bord" (visible Admin + Production), volontairement hors de la section "Administration" qui reste Admin-only (Utilisateurs, Paramètres).Page de modération — UI
Répondu(+ tooltip + preview tronquée) sihasReply, sinon—.rowData.hasReply) :isApproved), Supprimer.!hasReply: Répondre (ouvrekt_comments_modal_reply→POST /Comments/Reply).hasReply: Modifier la réponse (réutilisekt_comments_modal_editviadata-edit-target="reply", prefillreplyComment, target =replyId, va surPOST /Comments/Edit) + Supprimer la réponse (POST /Comments/DeleteReply).hasReply.Points d'attention / pièges
Restrict— toute logique de suppression doit cascader manuellement. Ne pas changer ça côté EF sans tester (SQL Server refuse cascade self-ref, c'est intentionnel).Authorlibre n'est pas garanti unique ni typé —"Buchard Voyages"n'est PAS un marqueur fiable pour différencier reply ≠ commentaire racine. Toujours utiliserParentCommentIdcomme source de vérité.TravelCommentDto ↔ TravelCommentest un ReverseMap() brut — toute nouvelle propriété sur le DTO sera automatiquement mappée. Si on ajoute des champs sensibles côté DTO un jour, refaire le ReverseMap manuellement.Travel.CommentsAverageRatingignore les replies naturellement (elles ontRating = null), mais si on les expose un jour côté API/front, vérifier qu'on n'inclut pas par accidentRepliesdans le compte.dotnet ef 6.0.9vs runtime 8.0.7 génère systématiquement desAlterColumnparasites surAspNetUserTokens/AspNetUserLogins. Convention du projet : les supprimer manuellement de la migration générée (cf.20260518085341_AddTravelCommentParent.cs).Conventions locales
_ListActions.cshtml: classesmenu-item-approve/menu-item-disapprove/menu-item-reply/menu-item-edit-reply/menu-item-delete-replytogglées en JS dansIndex.cshtml.OnPostXxx(Guid id, [FromForm] ...)retourneOkResult()/BadRequest(errors)consommé par AJAX (toaster + reload datatable).Dépendances
Travel(FK),Customer(FK nullable).Travel.CommentsAverageRating/Travel.CommentsCount(NotMapped getters surTravel).OnGetPendingCountconsommé par_Layout.cshtml > refreshCommentsPendingBadge.Contributors
Changelog