mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
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.
This commit is contained in:
parent
1eaf9b7c84
commit
16b31e5f28
7 changed files with 423 additions and 13 deletions
|
|
@ -33,5 +33,27 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
{
|
{
|
||||||
return await _context.Users.AnyAsync(u => u.Id == id);
|
return await _context.Users.AnyAsync(u => u.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyDictionary<string, string>> GetDisplayNamesByIdsAsync(IEnumerable<string> ids)
|
||||||
|
{
|
||||||
|
var idList = ids
|
||||||
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (idList.Count == 0)
|
||||||
|
return new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var users = await _context.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(u => idList.Contains(u.Id))
|
||||||
|
.Select(u => new { u.Id, u.FirstName, u.LastName })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return users.ToDictionary(
|
||||||
|
u => u.Id,
|
||||||
|
u => $"{u.FirstName ?? ""} {u.LastName ?? ""}".Trim(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,6 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
Task<IEnumerable<ApplicationUser>> GetAllAsync();
|
Task<IEnumerable<ApplicationUser>> GetAllAsync();
|
||||||
Task<ApplicationUser?> GetByEmailAsync(string email);
|
Task<ApplicationUser?> GetByEmailAsync(string email);
|
||||||
Task<bool> ExistsAsync(string id);
|
Task<bool> ExistsAsync(string id);
|
||||||
|
Task<IReadOnlyDictionary<string, string>> GetDisplayNamesByIdsAsync(IEnumerable<string> ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,69 @@ namespace SeaHaven.Services.Helpers
|
||||||
{
|
{
|
||||||
public static class WorkOrderAuditProjection
|
public static class WorkOrderAuditProjection
|
||||||
{
|
{
|
||||||
public static WorkOrderAuditEntryDto ToDto(WorkOrderAuditLog log)
|
private const string UnassignedLabel = "Unassigned";
|
||||||
|
|
||||||
|
public static WorkOrderAuditEntryDto ToDto(
|
||||||
|
WorkOrderAuditLog log,
|
||||||
|
IReadOnlyDictionary<string, string>? assignToDisplayNames = null)
|
||||||
{
|
{
|
||||||
|
var oldValue = log.OldValue ?? "";
|
||||||
|
var newValue = log.NewValue ?? "";
|
||||||
|
|
||||||
|
if (IsAssignToField(log.FieldName))
|
||||||
|
{
|
||||||
|
oldValue = ResolveAssignToDisplay(oldValue, assignToDisplayNames);
|
||||||
|
newValue = ResolveAssignToDisplay(newValue, assignToDisplayNames);
|
||||||
|
}
|
||||||
|
|
||||||
return new WorkOrderAuditEntryDto
|
return new WorkOrderAuditEntryDto
|
||||||
{
|
{
|
||||||
Type = MapEventType(log.EventType),
|
Type = MapEventType(log.EventType),
|
||||||
DispatcherId = log.UserId,
|
DispatcherId = log.UserId,
|
||||||
Action = log.Action ?? "",
|
Action = log.Action ?? "",
|
||||||
FieldName = log.FieldName,
|
FieldName = log.FieldName,
|
||||||
OldValue = log.OldValue ?? "",
|
OldValue = oldValue,
|
||||||
NewValue = log.NewValue ?? "",
|
NewValue = newValue,
|
||||||
Time = log.CreatedAt.ToString("o"),
|
Time = log.CreatedAt.ToString("o"),
|
||||||
DispatchId = log.DispatchId
|
DispatchId = log.DispatchId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<WorkOrderAuditEntryDto> ToDtos(IEnumerable<WorkOrderAuditLog> logs)
|
public static List<WorkOrderAuditEntryDto> ToDtos(
|
||||||
=> logs.Select(ToDto).ToList();
|
IEnumerable<WorkOrderAuditLog> logs,
|
||||||
|
IReadOnlyDictionary<string, string>? assignToDisplayNames = null)
|
||||||
|
=> logs.Select(log => ToDto(log, assignToDisplayNames)).ToList();
|
||||||
|
|
||||||
|
public static IEnumerable<string> CollectAssignToUserIds(IEnumerable<WorkOrderAuditLog> logs)
|
||||||
|
{
|
||||||
|
return logs
|
||||||
|
.Where(l => IsAssignToField(l.FieldName))
|
||||||
|
.SelectMany(l => new[] { l.OldValue, l.NewValue })
|
||||||
|
.Where(v => !string.IsNullOrWhiteSpace(v))
|
||||||
|
.Select(v => v!)
|
||||||
|
.Distinct(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsAssignToField(string? fieldName)
|
||||||
|
=> string.Equals(fieldName, "AssignTo", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(fieldName, "AssignedTo", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static string ResolveAssignToDisplay(
|
||||||
|
string value,
|
||||||
|
IReadOnlyDictionary<string, string>? assignToDisplayNames)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return UnassignedLabel;
|
||||||
|
|
||||||
|
if (assignToDisplayNames != null
|
||||||
|
&& assignToDisplayNames.TryGetValue(value, out var name)
|
||||||
|
&& !string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private static string MapEventType(string? eventType)
|
private static string MapEventType(string? eventType)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,48 @@ namespace SeaHaven.Services.Helpers
|
||||||
{
|
{
|
||||||
public static class WorkOrderCommentProjection
|
public static class WorkOrderCommentProjection
|
||||||
{
|
{
|
||||||
public static WorkOrderCommentDto ToDto(Comments comment)
|
public static WorkOrderCommentDto ToDto(
|
||||||
|
Comments comment,
|
||||||
|
IReadOnlyDictionary<string, string>? displayNames = null)
|
||||||
{
|
{
|
||||||
return new WorkOrderCommentDto
|
return new WorkOrderCommentDto
|
||||||
{
|
{
|
||||||
Id = comment.Id,
|
Id = comment.Id,
|
||||||
AuthorId = comment.UserId,
|
AuthorId = comment.UserId,
|
||||||
|
AuthorName = ResolveAuthorName(comment, displayNames),
|
||||||
Text = comment.Commenttext ?? "",
|
Text = comment.Commenttext ?? "",
|
||||||
Time = (comment.CreatedDate ?? DateTime.UtcNow).ToUniversalTime().ToString("o"),
|
Time = (comment.CreatedDate ?? DateTime.UtcNow).ToUniversalTime().ToString("o"),
|
||||||
Documents = comment.Documents
|
Documents = comment.Documents
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<WorkOrderCommentDto> ToDtos(IEnumerable<Comments> comments)
|
public static List<WorkOrderCommentDto> ToDtos(
|
||||||
=> comments.Select(ToDto).ToList();
|
IEnumerable<Comments> comments,
|
||||||
|
IReadOnlyDictionary<string, string>? displayNames = null)
|
||||||
|
=> comments.Select(c => ToDto(c, displayNames)).ToList();
|
||||||
|
|
||||||
|
public static IEnumerable<string> CollectAuthorUserIds(IEnumerable<Comments> comments)
|
||||||
|
=> comments
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c.UserId))
|
||||||
|
.Select(c => c.UserId!)
|
||||||
|
.Distinct(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
private static string? ResolveAuthorName(
|
||||||
|
Comments comment,
|
||||||
|
IReadOnlyDictionary<string, string>? displayNames)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(comment.UserId)
|
||||||
|
&& displayNames != null
|
||||||
|
&& displayNames.TryGetValue(comment.UserId, out var name)
|
||||||
|
&& !string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(comment.Commenter))
|
||||||
|
return comment.Commenter.Trim();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,16 @@ namespace SeaHaven.Services.Implementation
|
||||||
{
|
{
|
||||||
private readonly IWorkOrderDetailDataService _detailData;
|
private readonly IWorkOrderDetailDataService _detailData;
|
||||||
private readonly ICommentDataService _commentData;
|
private readonly ICommentDataService _commentData;
|
||||||
|
private readonly IUserDataService _userDataService;
|
||||||
|
|
||||||
public WorkOrderCommentService(
|
public WorkOrderCommentService(
|
||||||
IWorkOrderDetailDataService detailData,
|
IWorkOrderDetailDataService detailData,
|
||||||
ICommentDataService commentData)
|
ICommentDataService commentData,
|
||||||
|
IUserDataService userDataService)
|
||||||
{
|
{
|
||||||
_detailData = detailData;
|
_detailData = detailData;
|
||||||
_commentData = commentData;
|
_commentData = commentData;
|
||||||
|
_userDataService = userDataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<WorkOrderCommentDto>?> GetCommentsAsync(int workOrderId)
|
public async Task<IReadOnlyList<WorkOrderCommentDto>?> GetCommentsAsync(int workOrderId)
|
||||||
|
|
@ -26,7 +29,8 @@ namespace SeaHaven.Services.Implementation
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var comments = await _detailData.GetCommentsAsync(workOrderId);
|
var comments = await _detailData.GetCommentsAsync(workOrderId);
|
||||||
return WorkOrderCommentProjection.ToDtos(comments);
|
var authorNames = await ResolveAuthorNamesAsync(comments);
|
||||||
|
return WorkOrderCommentProjection.ToDtos(comments, authorNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WorkOrderCommentDto> AddCommentAsync(
|
public async Task<WorkOrderCommentDto> AddCommentAsync(
|
||||||
|
|
@ -53,7 +57,58 @@ namespace SeaHaven.Services.Implementation
|
||||||
};
|
};
|
||||||
|
|
||||||
var saved = await _commentData.AddAsync(comment);
|
var saved = await _commentData.AddAsync(comment);
|
||||||
return WorkOrderCommentProjection.ToDto(saved);
|
var authorNames = await ResolveAuthorNamesAsync(new[] { saved });
|
||||||
|
return WorkOrderCommentProjection.ToDto(saved, authorNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorkOrderCommentDto> UpdateCommentAsync(
|
||||||
|
int workOrderId,
|
||||||
|
int commentId,
|
||||||
|
WorkOrderCommentCreateDto request,
|
||||||
|
string? actorId,
|
||||||
|
bool isAdmin)
|
||||||
|
{
|
||||||
|
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 && !isAdmin)
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,11 @@ namespace SeaHaven.Services.Interfaces
|
||||||
{
|
{
|
||||||
Task<IReadOnlyList<WorkOrderCommentDto>?> GetCommentsAsync(int workOrderId);
|
Task<IReadOnlyList<WorkOrderCommentDto>?> GetCommentsAsync(int workOrderId);
|
||||||
Task<WorkOrderCommentDto> AddCommentAsync(int workOrderId, WorkOrderCommentCreateDto request, string? actorId, string? documentUrl = null);
|
Task<WorkOrderCommentDto> AddCommentAsync(int workOrderId, WorkOrderCommentCreateDto request, string? actorId, string? documentUrl = null);
|
||||||
|
Task<WorkOrderCommentDto> UpdateCommentAsync(
|
||||||
|
int workOrderId,
|
||||||
|
int commentId,
|
||||||
|
WorkOrderCommentCreateDto request,
|
||||||
|
string? actorId,
|
||||||
|
bool isAdmin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,8 @@ public class WorkOrderDetailServiceTests
|
||||||
var boardService = new WorkOrderBoardService(boardData);
|
var boardService = new WorkOrderBoardService(boardData);
|
||||||
var detailData = new WorkOrderDetailDataService(context);
|
var detailData = new WorkOrderDetailDataService(context);
|
||||||
var templateData = new CompletionDocTemplateDataService(context);
|
var templateData = new CompletionDocTemplateDataService(context);
|
||||||
var service = new WorkOrderDetailService(boardService, detailData, templateData);
|
var userData = new UserDataService(context);
|
||||||
|
var service = new WorkOrderDetailService(boardService, detailData, templateData, userData);
|
||||||
return (context, service);
|
return (context, service);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,6 +132,13 @@ public class WorkOrderDetailServiceTests
|
||||||
NewValue = "1",
|
NewValue = "1",
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
|
context.Users.Add(new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = "user-1",
|
||||||
|
UserName = "alice",
|
||||||
|
FirstName = "Alice",
|
||||||
|
LastName = "Dispatcher"
|
||||||
|
});
|
||||||
context.Comments.Add(new Comments
|
context.Comments.Add(new Comments
|
||||||
{
|
{
|
||||||
WorkerOrderId = 1,
|
WorkerOrderId = 1,
|
||||||
|
|
@ -150,11 +158,75 @@ public class WorkOrderDetailServiceTests
|
||||||
Assert.Equal("HVAC PM Completion", detail.Completion.Template!.Name);
|
Assert.Equal("HVAC PM Completion", detail.Completion.Template!.Name);
|
||||||
Assert.Single(detail.Comments);
|
Assert.Single(detail.Comments);
|
||||||
Assert.Equal("user-1", detail.Comments[0].AuthorId);
|
Assert.Equal("user-1", detail.Comments[0].AuthorId);
|
||||||
|
Assert.Equal("Alice Dispatcher", detail.Comments[0].AuthorName);
|
||||||
Assert.Equal("Called vendor", detail.Comments[0].Text);
|
Assert.Equal("Called vendor", detail.Comments[0].Text);
|
||||||
Assert.Single(detail.Audit);
|
Assert.Single(detail.Audit);
|
||||||
Assert.Equal("system", detail.Audit[0].Type);
|
Assert.Equal("system", detail.Audit[0].Type);
|
||||||
Assert.Equal("WeekRolled", detail.Audit[0].Action);
|
Assert.Equal("WeekRolled", detail.Audit[0].Action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAudit_ResolvesAssignToUserIdsToDisplayNames()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
const string oldUserId = "a1000001-0001-4000-8000-000000000002";
|
||||||
|
const string newUserId = "b5e356d5-d926-4f92-8673-ee13cddeff0f";
|
||||||
|
|
||||||
|
context.Users.AddRange(
|
||||||
|
new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = oldUserId,
|
||||||
|
UserName = "alice",
|
||||||
|
FirstName = "Alice",
|
||||||
|
LastName = "Dispatcher"
|
||||||
|
},
|
||||||
|
new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = newUserId,
|
||||||
|
UserName = "bob",
|
||||||
|
FirstName = "Bob",
|
||||||
|
LastName = "Tech"
|
||||||
|
});
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "00000000001",
|
||||||
|
LifecycleStatus = LifecycleStatus.Pending,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
});
|
||||||
|
context.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||||
|
{
|
||||||
|
WorkOrderId = 1,
|
||||||
|
UserId = "actor-1",
|
||||||
|
EventType = "Manual",
|
||||||
|
Action = "AssignmentChanged",
|
||||||
|
FieldName = "AssignTo",
|
||||||
|
OldValue = oldUserId,
|
||||||
|
NewValue = newUserId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
context.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||||
|
{
|
||||||
|
WorkOrderId = 1,
|
||||||
|
UserId = "actor-1",
|
||||||
|
EventType = "Manual",
|
||||||
|
Action = "AssignmentChanged",
|
||||||
|
FieldName = "AssignTo",
|
||||||
|
OldValue = newUserId,
|
||||||
|
NewValue = "",
|
||||||
|
CreatedAt = DateTime.UtcNow.AddMinutes(1)
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var audit = await service.GetAuditAsync(1);
|
||||||
|
|
||||||
|
Assert.NotNull(audit);
|
||||||
|
Assert.Equal(2, audit!.Count);
|
||||||
|
var unassigned = audit.Single(a => a.NewValue == "Unassigned");
|
||||||
|
Assert.Equal("Bob Tech", unassigned.OldValue);
|
||||||
|
var reassigned = audit.Single(a => a.NewValue == "Bob Tech");
|
||||||
|
Assert.Equal("Alice Dispatcher", reassigned.OldValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WorkOrderCompletionServiceTests
|
public class WorkOrderCompletionServiceTests
|
||||||
|
|
@ -306,18 +378,197 @@ public class WorkOrderCommentServiceTests
|
||||||
.Options;
|
.Options;
|
||||||
var context = new ApplicationDbContext(options);
|
var context = new ApplicationDbContext(options);
|
||||||
context.workOrders.Add(new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled });
|
context.workOrders.Add(new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled });
|
||||||
|
context.Users.Add(new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = "user-abc",
|
||||||
|
UserName = "bob",
|
||||||
|
FirstName = "Bob",
|
||||||
|
LastName = "Tech"
|
||||||
|
});
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var detailData = new WorkOrderDetailDataService(context);
|
var detailData = new WorkOrderDetailDataService(context);
|
||||||
var commentData = new CommentDataService(context);
|
var commentData = new CommentDataService(context);
|
||||||
var service = new WorkOrderCommentService(detailData, commentData);
|
var userData = new UserDataService(context);
|
||||||
|
var service = new WorkOrderCommentService(detailData, commentData, userData);
|
||||||
|
|
||||||
var result = await service.AddCommentAsync(1, new WorkOrderCommentCreateDto { Text = "Note" }, "user-abc");
|
var result = await service.AddCommentAsync(1, new WorkOrderCommentCreateDto { Text = "Note" }, "user-abc");
|
||||||
|
|
||||||
Assert.Equal("user-abc", result.AuthorId);
|
Assert.Equal("user-abc", result.AuthorId);
|
||||||
|
Assert.Equal("Bob Tech", result.AuthorName);
|
||||||
Assert.Equal("Note", result.Text);
|
Assert.Equal("Note", result.Text);
|
||||||
Assert.False(string.IsNullOrWhiteSpace(result.Time));
|
Assert.False(string.IsNullOrWhiteSpace(result.Time));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetComments_FallsBackToCommenterWhenUserIdMissing()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||||
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
var context = new ApplicationDbContext(options);
|
||||||
|
context.workOrders.Add(new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled });
|
||||||
|
context.Comments.Add(new Comments
|
||||||
|
{
|
||||||
|
WorkerOrderId = 1,
|
||||||
|
Commenter = "Legacy Sync Author",
|
||||||
|
Commenttext = "Synced note",
|
||||||
|
CreatedDate = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var detailData = new WorkOrderDetailDataService(context);
|
||||||
|
var commentData = new CommentDataService(context);
|
||||||
|
var userData = new UserDataService(context);
|
||||||
|
var service = new WorkOrderCommentService(detailData, commentData, userData);
|
||||||
|
|
||||||
|
var comments = await service.GetCommentsAsync(1);
|
||||||
|
|
||||||
|
Assert.NotNull(comments);
|
||||||
|
Assert.Single(comments!);
|
||||||
|
Assert.Null(comments[0].AuthorId);
|
||||||
|
Assert.Equal("Legacy Sync Author", comments[0].AuthorName);
|
||||||
|
Assert.Equal("Synced note", comments[0].Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<(ApplicationDbContext Context, WorkOrderCommentService Service, Comments Comment)> SeedEditableCommentAsync(
|
||||||
|
LifecycleStatus status = LifecycleStatus.Scheduled,
|
||||||
|
string authorId = "user-author",
|
||||||
|
string commentType = "General",
|
||||||
|
string recordType = "WorkOrder",
|
||||||
|
int workOrderId = 1)
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||||
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
var context = new ApplicationDbContext(options);
|
||||||
|
context.workOrders.Add(new WorkOrder { Id = workOrderId, LifecycleStatus = status });
|
||||||
|
context.Users.Add(new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = authorId,
|
||||||
|
UserName = "author",
|
||||||
|
FirstName = "Alice",
|
||||||
|
LastName = "Dispatcher"
|
||||||
|
});
|
||||||
|
var comment = new Comments
|
||||||
|
{
|
||||||
|
WorkerOrderId = workOrderId,
|
||||||
|
UserId = authorId,
|
||||||
|
Commenttext = "Original",
|
||||||
|
CommentType = commentType,
|
||||||
|
RecordType = recordType,
|
||||||
|
CreatedDate = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
context.Comments.Add(comment);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new WorkOrderCommentService(
|
||||||
|
new WorkOrderDetailDataService(context),
|
||||||
|
new CommentDataService(context),
|
||||||
|
new UserDataService(context));
|
||||||
|
return (context, service, comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateComment_Author_UpdatesText()
|
||||||
|
{
|
||||||
|
var (_, service, comment) = await SeedEditableCommentAsync();
|
||||||
|
|
||||||
|
var result = await service.UpdateCommentAsync(
|
||||||
|
1,
|
||||||
|
comment.Id,
|
||||||
|
new WorkOrderCommentCreateDto { Text = " Updated note " },
|
||||||
|
"user-author",
|
||||||
|
isAdmin: false);
|
||||||
|
|
||||||
|
Assert.Equal("Updated note", result.Text);
|
||||||
|
Assert.Equal("user-author", result.AuthorId);
|
||||||
|
Assert.Equal("Alice Dispatcher", result.AuthorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateComment_Admin_CanEditOthers()
|
||||||
|
{
|
||||||
|
var (_, service, comment) = await SeedEditableCommentAsync();
|
||||||
|
|
||||||
|
var result = await service.UpdateCommentAsync(
|
||||||
|
1,
|
||||||
|
comment.Id,
|
||||||
|
new WorkOrderCommentCreateDto { Text = "Admin edit" },
|
||||||
|
"admin-user",
|
||||||
|
isAdmin: true);
|
||||||
|
|
||||||
|
Assert.Equal("Admin edit", result.Text);
|
||||||
|
Assert.Equal("user-author", result.AuthorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateComment_NonAuthorNonAdmin_ThrowsForbidden()
|
||||||
|
{
|
||||||
|
var (_, service, comment) = await SeedEditableCommentAsync();
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||||
|
service.UpdateCommentAsync(
|
||||||
|
1,
|
||||||
|
comment.Id,
|
||||||
|
new WorkOrderCommentCreateDto { Text = "Nope" },
|
||||||
|
"other-user",
|
||||||
|
isAdmin: false));
|
||||||
|
|
||||||
|
Assert.Equal("Forbidden", ex.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateComment_LegacyVendorComment_ThrowsNotEditable()
|
||||||
|
{
|
||||||
|
var (_, service, comment) = await SeedEditableCommentAsync(
|
||||||
|
commentType: "vendor",
|
||||||
|
recordType: "WorkOrder");
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||||
|
service.UpdateCommentAsync(
|
||||||
|
1,
|
||||||
|
comment.Id,
|
||||||
|
new WorkOrderCommentCreateDto { Text = "Nope" },
|
||||||
|
"user-author",
|
||||||
|
isAdmin: false));
|
||||||
|
|
||||||
|
Assert.Equal("NotEditable", ex.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateComment_ReadOnlyWorkOrder_Throws()
|
||||||
|
{
|
||||||
|
var (_, service, comment) = await SeedEditableCommentAsync(LifecycleStatus.Canceled);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||||
|
service.UpdateCommentAsync(
|
||||||
|
1,
|
||||||
|
comment.Id,
|
||||||
|
new WorkOrderCommentCreateDto { Text = "Nope" },
|
||||||
|
"user-author",
|
||||||
|
isAdmin: false));
|
||||||
|
|
||||||
|
Assert.Equal("ReadOnly", ex.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateComment_WrongWorkOrder_ThrowsNotFound()
|
||||||
|
{
|
||||||
|
var (context, service, comment) = await SeedEditableCommentAsync(workOrderId: 1);
|
||||||
|
context.workOrders.Add(new WorkOrder { Id = 2, LifecycleStatus = LifecycleStatus.Scheduled });
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||||
|
service.UpdateCommentAsync(
|
||||||
|
2,
|
||||||
|
comment.Id,
|
||||||
|
new WorkOrderCommentCreateDto { Text = "Nope" },
|
||||||
|
"user-author",
|
||||||
|
isAdmin: false));
|
||||||
|
|
||||||
|
Assert.Equal("NotFound", ex.Code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WorkOrderMediaServiceTests
|
public class WorkOrderMediaServiceTests
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue