shoc-backend/SeaHaven.Services/Implementation/WorkOrderCommentService.cs
Arthur Bassi 8f492c0faf
feat(work-orders): allow comment edit and resolve author audit display names (#24)
* feat(work-orders): enrich board search overdue filters and 0-based paging

* fix(work-orders): align stacked services with CI build

* fix(tests): pass userDataService in comment service unit test

* fix(work-orders): use dedicated overdue query flag

Stop treating WorkOrderType.Other as an overdue sentinel. Board and advanced search now accept overdue=true while types=Other filters real Other rows; combining both uses OR.

* feat(work-orders): allow comment edit and resolve author audit display names

Add PATCH comment for author/Admin, return authorName, and resolve
AssignTo audit values to user display names.

* fix(work-orders): enforce author-only comment edits per SH-122

Remove the undocumented Admin override so only the original comment author can edit, matching the ticket acceptance criteria.

---------

Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com>
Co-authored-by: Alexandre Brandizzi <alex_brandizzi@hotmail.com>
2026-07-24 21:28:44 +00:00

113 lines
4.8 KiB
C#

using Data.SeaHavenIndustries;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Exceptions;
using SeaHaven.Services.Helpers;
using SeaHaven.Services.Interfaces;
namespace SeaHaven.Services.Implementation
{
public class WorkOrderCommentService : IWorkOrderCommentService
{
private readonly IWorkOrderDetailDataService _detailData;
private readonly ICommentDataService _commentData;
private readonly IUserDataService _userDataService;
public WorkOrderCommentService(
IWorkOrderDetailDataService detailData,
ICommentDataService commentData,
IUserDataService userDataService)
{
_detailData = detailData;
_commentData = commentData;
_userDataService = userDataService;
}
public async Task<IReadOnlyList<WorkOrderCommentDto>?> GetCommentsAsync(int workOrderId)
{
if (!await _detailData.ExistsAsync(workOrderId))
return null;
var comments = await _detailData.GetCommentsAsync(workOrderId);
var authorNames = await ResolveAuthorNamesAsync(comments);
return WorkOrderCommentProjection.ToDtos(comments, authorNames);
}
public async Task<WorkOrderCommentDto> AddCommentAsync(
int workOrderId,
WorkOrderCommentCreateDto request,
string? actorId,
string? documentUrl = null)
{
if (!await _detailData.ExistsAsync(workOrderId))
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
if (string.IsNullOrWhiteSpace(request.Text))
throw new WorkOrderBoardValidationException("InvalidValue", "Comment text is required.");
var comment = new Comments
{
WorkerOrderId = workOrderId,
UserId = actorId,
Commenttext = request.Text.Trim(),
CommentType = "General",
RecordType = "WorkOrder",
Documents = documentUrl,
CreatedDate = DateTime.UtcNow
};
var saved = await _commentData.AddAsync(comment);
var authorNames = await ResolveAuthorNamesAsync(new[] { saved });
return WorkOrderCommentProjection.ToDto(saved, authorNames);
}
public async Task<WorkOrderCommentDto> UpdateCommentAsync(
int workOrderId,
int commentId,
WorkOrderCommentCreateDto request,
string? actorId)
{
var workOrder = await _detailData.GetWorkOrderForMediaAsync(workOrderId);
if (workOrder == null)
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status.");
if (string.IsNullOrWhiteSpace(request.Text))
throw new WorkOrderBoardValidationException("InvalidValue", "Comment text is required.");
var comment = await _commentData.GetByIdAsync(commentId);
if (comment == null || comment.WorkerOrderId != workOrderId || comment.IsDeleted == true)
throw new WorkOrderBoardValidationException("NotFound", "Comment not found.");
if (!IsBoardEditableComment(comment))
throw new WorkOrderBoardValidationException(
"NotEditable",
"Only board comments (CommentType General) can be edited via this endpoint.");
var isAuthor = !string.IsNullOrWhiteSpace(actorId)
&& string.Equals(comment.UserId, actorId, StringComparison.Ordinal);
if (!isAuthor)
throw new WorkOrderBoardValidationException("Forbidden", "You are not allowed to edit this comment.");
comment.Commenttext = request.Text.Trim();
comment.LastModificationTime = DateTime.UtcNow;
await _commentData.UpdateAsync(comment);
var authorNames = await ResolveAuthorNamesAsync(new[] { comment });
return WorkOrderCommentProjection.ToDto(comment, authorNames);
}
private static bool IsBoardEditableComment(Comments comment)
=> string.Equals(comment.CommentType, "General", StringComparison.OrdinalIgnoreCase)
&& string.Equals(comment.RecordType, "WorkOrder", StringComparison.OrdinalIgnoreCase);
private async Task<IReadOnlyDictionary<string, string>> ResolveAuthorNamesAsync(
IEnumerable<Comments> comments)
{
var authorIds = WorkOrderCommentProjection.CollectAuthorUserIds(comments);
return await _userDataService.GetDisplayNamesByIdsAsync(authorIds);
}
}
}