From 4b8a08fb104ebdba082c3f3c11be614b34e742d7 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Mon, 24 Aug 2026 09:59:13 -0300 Subject: [PATCH] fix(work-orders): block comment creation on canceled work orders --- .../Implementation/WorkOrderCommentService.cs | 16 ++++- .../WorkOrderCompletedSelectiveLockTests.cs | 65 +++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/SeaHaven.Services/Implementation/WorkOrderCommentService.cs b/SeaHaven.Services/Implementation/WorkOrderCommentService.cs index 77c17d4..c039cfc 100644 --- a/SeaHaven.Services/Implementation/WorkOrderCommentService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderCommentService.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; @@ -48,9 +49,13 @@ namespace SeaHaven.Services.Implementation string? documentUrl = null) { var accountId = _accountResolver.ResolveAccountFilter(user); - if (!await _detailData.ExistsAsync(workOrderId, CancellationToken.None, accountId)) + var workOrder = await _detailData.GetWorkOrderForMediaAsync( + workOrderId, CancellationToken.None, accountId); + if (workOrder == null) throw new WorkOrderBoardValidationException("NotFound", "Work order not found."); + EnsureCommentMutationAllowed(workOrder.LifecycleStatus); + if (string.IsNullOrWhiteSpace(request.Text)) throw new WorkOrderBoardValidationException("InvalidValue", "Comment text is required."); @@ -83,8 +88,7 @@ namespace SeaHaven.Services.Implementation if (workOrder == null) throw new WorkOrderBoardValidationException("NotFound", "Work order not found."); - if (!WorkOrderBoardMutationRules.CanMutateComments(workOrder.LifecycleStatus)) - throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status."); + EnsureCommentMutationAllowed(workOrder.LifecycleStatus); if (string.IsNullOrWhiteSpace(request.Text)) throw new WorkOrderBoardValidationException("InvalidValue", "Comment text is required."); @@ -111,6 +115,12 @@ namespace SeaHaven.Services.Implementation return WorkOrderCommentProjection.ToDto(comment, authorNames); } + private static void EnsureCommentMutationAllowed(LifecycleStatus? status) + { + if (!WorkOrderBoardMutationRules.CanMutateComments(status)) + throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status."); + } + private static bool IsBoardEditableComment(Comments comment) => string.Equals(comment.CommentType, "General", StringComparison.OrdinalIgnoreCase) && string.Equals(comment.RecordType, "WorkOrder", StringComparison.OrdinalIgnoreCase); diff --git a/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs b/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs index 0bf330e..9ebd221 100644 --- a/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs @@ -159,6 +159,71 @@ public class WorkOrderCompletedSelectiveLockTests Assert.Equal("Follow-up", result.Text); } + [Fact] + public async Task AddComment_Completed_AddsText() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var context = new ApplicationDbContext(options); + context.workOrders.Add(CompletedWorkOrder()); + context.Users.Add(new ApplicationUser + { + Id = "user-author", + UserName = "author", + FirstName = "Alice", + LastName = "Dispatcher" + }); + await context.SaveChangesAsync(); + + var service = new WorkOrderCommentService( + new WorkOrderDetailDataService(context), + new CommentDataService(context), + new UserDataService(context), + WorkOrderAccountTestHelpers.Resolver(context)); + + var result = await service.AddCommentAsync( + 1, + new WorkOrderCommentCreateDto { Text = "Follow-up" }, + WorkOrderAccountTestHelpers.OrgWideAdmin("user-author"), + "user-author"); + + Assert.Equal("Follow-up", result.Text); + Assert.Equal("user-author", result.AuthorId); + } + + [Fact] + public async Task AddComment_Canceled_ThrowsReadOnly() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var context = new ApplicationDbContext(options); + context.workOrders.Add(new WorkOrder + { + Id = 1, + LifecycleStatus = LifecycleStatus.Canceled, + RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } + }); + await context.SaveChangesAsync(); + + var service = new WorkOrderCommentService( + new WorkOrderDetailDataService(context), + new CommentDataService(context), + new UserDataService(context), + WorkOrderAccountTestHelpers.Resolver(context)); + + var ex = await Assert.ThrowsAsync(() => + service.AddCommentAsync( + 1, + new WorkOrderCommentCreateDto { Text = "Should not persist" }, + WorkOrderAccountTestHelpers.OrgWideAdmin("user-author"), + "user-author")); + + Assert.Equal("ReadOnly", ex.Code); + Assert.Empty(context.Comments); + } + [Fact] public async Task AddMedia_Completed_AllowsExtra() {