fix(work-orders): block comment creation on canceled work orders

This commit is contained in:
Arthur Bassi 2026-08-24 09:59:13 -03:00
parent f47264ec4d
commit 4b8a08fb10
2 changed files with 78 additions and 3 deletions

View file

@ -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);

View file

@ -159,6 +159,71 @@ public class WorkOrderCompletedSelectiveLockTests
Assert.Equal("Follow-up", result.Text);
}
[Fact]
public async Task AddComment_Completed_AddsText()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.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<ApplicationDbContext>()
.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<WorkOrderBoardValidationException>(() =>
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()
{