From a80a71b16af6fe93e9121c4314e21950b7858ad2 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Fri, 17 Jul 2026 13:18:43 -0300 Subject: [PATCH 1/3] fix(work-orders): make stacked tip build for CI quality gates --- .../WorkOrderControllerSearchTests.cs | 6 +- .../Implementation/UserDataService.cs | 22 ++++++++ .../Interfaces/IUserDataService.cs | 1 + .../Helpers/WorkOrderAuditProjection.cs | 56 +++++++++++++++++-- .../Helpers/WorkOrderCommentProjection.cs | 35 +++++++++++- .../Helpers/WorkOrderFlagColors.cs | 50 +++++++++++++++++ 6 files changed, 161 insertions(+), 9 deletions(-) create mode 100644 SeaHaven.Services/Helpers/WorkOrderFlagColors.cs diff --git a/Api.SeaHavenIndustries.Tests/WorkOrderControllerSearchTests.cs b/Api.SeaHavenIndustries.Tests/WorkOrderControllerSearchTests.cs index 61d37c9..17826af 100644 --- a/Api.SeaHavenIndustries.Tests/WorkOrderControllerSearchTests.cs +++ b/Api.SeaHavenIndustries.Tests/WorkOrderControllerSearchTests.cs @@ -59,7 +59,11 @@ public class WorkOrderControllerSearchTests Mock.Of(), Mock.Of(), Mock.Of(), - advancedSearch); + advancedSearch, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); controller.ControllerContext = new ControllerContext { diff --git a/SeaHaven.DataServices/Implementation/UserDataService.cs b/SeaHaven.DataServices/Implementation/UserDataService.cs index 365ceca..436a806 100644 --- a/SeaHaven.DataServices/Implementation/UserDataService.cs +++ b/SeaHaven.DataServices/Implementation/UserDataService.cs @@ -33,5 +33,27 @@ namespace SeaHaven.DataServices.Implementation { return await _context.Users.AnyAsync(u => u.Id == id); } + + public async Task> GetDisplayNamesByIdsAsync(IEnumerable ids) + { + var idList = ids + .Where(id => !string.IsNullOrWhiteSpace(id)) + .Distinct(StringComparer.Ordinal) + .ToList(); + + if (idList.Count == 0) + return new Dictionary(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); + } } } diff --git a/SeaHaven.DataServices/Interfaces/IUserDataService.cs b/SeaHaven.DataServices/Interfaces/IUserDataService.cs index 6524cc4..6cfed2d 100644 --- a/SeaHaven.DataServices/Interfaces/IUserDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IUserDataService.cs @@ -8,5 +8,6 @@ namespace SeaHaven.DataServices.Interfaces Task> GetAllAsync(); Task GetByEmailAsync(string email); Task ExistsAsync(string id); + Task> GetDisplayNamesByIdsAsync(IEnumerable ids); } } diff --git a/SeaHaven.Services/Helpers/WorkOrderAuditProjection.cs b/SeaHaven.Services/Helpers/WorkOrderAuditProjection.cs index 8b56b47..6a7e295 100644 --- a/SeaHaven.Services/Helpers/WorkOrderAuditProjection.cs +++ b/SeaHaven.Services/Helpers/WorkOrderAuditProjection.cs @@ -5,23 +5,69 @@ namespace SeaHaven.Services.Helpers { public static class WorkOrderAuditProjection { - public static WorkOrderAuditEntryDto ToDto(WorkOrderAuditLog log) + private const string UnassignedLabel = "Unassigned"; + + public static WorkOrderAuditEntryDto ToDto( + WorkOrderAuditLog log, + IReadOnlyDictionary? 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 { Type = MapEventType(log.EventType), DispatcherId = log.UserId, Action = log.Action ?? "", FieldName = log.FieldName, - OldValue = log.OldValue ?? "", - NewValue = log.NewValue ?? "", + OldValue = oldValue, + NewValue = newValue, Time = log.CreatedAt.ToString("o"), DispatchId = log.DispatchId }; } - public static List ToDtos(IEnumerable logs) - => logs.Select(ToDto).ToList(); + public static List ToDtos( + IEnumerable logs, + IReadOnlyDictionary? assignToDisplayNames = null) + => logs.Select(log => ToDto(log, assignToDisplayNames)).ToList(); + + public static IEnumerable CollectAssignToUserIds(IEnumerable 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? 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) { diff --git a/SeaHaven.Services/Helpers/WorkOrderCommentProjection.cs b/SeaHaven.Services/Helpers/WorkOrderCommentProjection.cs index 096edd5..cb4a2d5 100644 --- a/SeaHaven.Services/Helpers/WorkOrderCommentProjection.cs +++ b/SeaHaven.Services/Helpers/WorkOrderCommentProjection.cs @@ -5,19 +5,48 @@ namespace SeaHaven.Services.Helpers { public static class WorkOrderCommentProjection { - public static WorkOrderCommentDto ToDto(Comments comment) + public static WorkOrderCommentDto ToDto( + Comments comment, + IReadOnlyDictionary? displayNames = null) { return new WorkOrderCommentDto { Id = comment.Id, AuthorId = comment.UserId, + AuthorName = ResolveAuthorName(comment, displayNames), Text = comment.Commenttext ?? "", Time = (comment.CreatedDate ?? DateTime.UtcNow).ToUniversalTime().ToString("o"), Documents = comment.Documents }; } - public static List ToDtos(IEnumerable comments) - => comments.Select(ToDto).ToList(); + public static List ToDtos( + IEnumerable comments, + IReadOnlyDictionary? displayNames = null) + => comments.Select(c => ToDto(c, displayNames)).ToList(); + + public static IEnumerable CollectAuthorUserIds(IEnumerable comments) + => comments + .Where(c => !string.IsNullOrWhiteSpace(c.UserId)) + .Select(c => c.UserId!) + .Distinct(StringComparer.Ordinal); + + private static string? ResolveAuthorName( + Comments comment, + IReadOnlyDictionary? 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; + } } } diff --git a/SeaHaven.Services/Helpers/WorkOrderFlagColors.cs b/SeaHaven.Services/Helpers/WorkOrderFlagColors.cs new file mode 100644 index 0000000..6892bd8 --- /dev/null +++ b/SeaHaven.Services/Helpers/WorkOrderFlagColors.cs @@ -0,0 +1,50 @@ +namespace SeaHaven.Services.Helpers +{ + /// + /// Fixed board flag palette (must match FE FLAG_COLORS). Values stored as canonical #RRGGBB. + /// + public static class WorkOrderFlagColors + { + public static readonly HashSet Allowed = new(StringComparer.OrdinalIgnoreCase) + { + "#EF4444", + "#F97316", + "#F59E0B", + "#EAB308", + "#22C55E", + "#14B8A6", + "#3B82F6", + "#8B5CF6" + }; + + /// + /// Normalizes to # + 6 uppercase hex digits, or null if empty. + /// Returns false if the value is non-empty but not a valid hex color. + /// + public static bool TryNormalize(string? value, out string? canonical) + { + canonical = null; + if (string.IsNullOrWhiteSpace(value)) + return true; + + var trimmed = value.Trim(); + if (trimmed.Length == 6 && trimmed[0] != '#') + trimmed = "#" + trimmed; + + if (trimmed.Length != 7 || trimmed[0] != '#') + return false; + + for (var i = 1; i < 7; i++) + { + if (!Char.IsAsciiHexDigit(trimmed[i])) + return false; + } + + canonical = "#" + trimmed[1..].ToUpperInvariant(); + return true; + } + + public static bool IsAllowed(string? canonical) + => canonical != null && Allowed.Contains(canonical); + } +} From d59f92910e62228f15908c38598becfd735821b1 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Fri, 17 Jul 2026 13:38:56 -0300 Subject: [PATCH 2/3] fix(work-orders): align stacked services with CI build --- SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs index 343337e..85d0a98 100644 --- a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs @@ -92,7 +92,8 @@ public class WorkOrderDetailServiceTests var boardService = new WorkOrderBoardService(boardData); var detailData = new WorkOrderDetailDataService(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); } From 33574c233b94af729e986b3095cb008a9555ae5c Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Tue, 21 Jul 2026 09:38:34 -0300 Subject: [PATCH 3/3] fix(work-orders): keep FlagColor out of structured-notes slice Remove FlagColor model/API/tests from this PR so schema and EF model stay aligned with the structured-fields migration. FlagColor remains in the follow-on PR that adds its migration. --- .../ApplicationDbContextModelSnapshot.cs | 4 - Data.SeaHavenIndustries/Models/WorkerOrder.cs | 4 - .../Helpers/WorkOrderBoardProjection.cs | 2 - .../Interfaces/WorkOrderBoardModels.cs | 1 - SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs | 2 - .../Helpers/WorkOrderBoardFieldNames.cs | 5 +- .../Helpers/WorkOrderFlagColors.cs | 50 ------ .../Implementation/WorkOrderBoardService.cs | 1 - .../WorkOrderBoardUpdateService.cs | 19 --- .../Implementation/WorkOrderDetailService.cs | 1 - .../WorkOrderBoardUpdateServiceTests.cs | 153 ------------------ 11 files changed, 1 insertion(+), 241 deletions(-) delete mode 100644 SeaHaven.Services/Helpers/WorkOrderFlagColors.cs diff --git a/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs b/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs index 608754d..3da8116 100644 --- a/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1975,10 +1975,6 @@ namespace Data.SeaHavenIndustries.Migrations .HasMaxLength(2000) .HasColumnType("nvarchar(2000)"); - b.Property("FlagColor") - .HasMaxLength(7) - .HasColumnType("nvarchar(7)"); - b.Property("InternalWONumber") .HasColumnType("nvarchar(max)"); diff --git a/Data.SeaHavenIndustries/Models/WorkerOrder.cs b/Data.SeaHavenIndustries/Models/WorkerOrder.cs index 052c2d3..cb30f29 100644 --- a/Data.SeaHavenIndustries/Models/WorkerOrder.cs +++ b/Data.SeaHavenIndustries/Models/WorkerOrder.cs @@ -70,10 +70,6 @@ namespace Data.SeaHavenIndustries // --- Completion slice --- public DocStatus? DocStatus { get; set; } - /// Board flag color (#RRGGBB from fixed palette). Null = no flag. - [MaxLength(7)] - public string? FlagColor { get; set; } - // --- Legacy SQL columns (drift reconciliation) --- public string? AvettaTask { get; set; } public DateOnly? AssignDate { get; set; } diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs index 5520791..71c417a 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs @@ -66,7 +66,6 @@ namespace SeaHaven.DataServices.Helpers w.ServiceNotes, w.ExtraServices, w.DocStatus, - w.FlagColor, w.PrimaryDispatchId, w.RowVersion, DispatchRowVersion = w.PrimaryDispatch != null ? w.PrimaryDispatch.RowVersion : null @@ -116,7 +115,6 @@ namespace SeaHaven.DataServices.Helpers w.ServiceNotes, w.ExtraServices, w.DocStatus, - w.FlagColor, w.PrimaryDispatchId, w.RowVersion, w.DispatchRowVersion, diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs index c46ff55..ce96697 100644 --- a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs +++ b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs @@ -45,7 +45,6 @@ namespace SeaHaven.DataServices.Interfaces string? ServiceNotes, string? ExtraServices, DocStatus? DocStatus, - string? FlagColor, int? PrimaryDispatchId, byte[]? RowVersion, byte[]? DispatchRowVersion, diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs index f0b13f9..1f1b549 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs @@ -54,8 +54,6 @@ namespace SeaHaven.Services.DTOs public string? ServiceNotes { get; set; } public List? ExtraServices { get; set; } public DocStatus? DocStatus { get; set; } - /// Board flag color (#RRGGBB). Null = no flag. Distinct from Color (dispatcher avatar). - public string? FlagColor { get; set; } public int? PrimaryDispatchId { get; set; } public byte[]? RowVersion { get; set; } public byte[]? DispatchRowVersion { get; set; } diff --git a/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs b/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs index a76a7fe..233090e 100644 --- a/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs +++ b/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs @@ -18,7 +18,6 @@ namespace SeaHaven.Services.Helpers public const string Pm = "pm"; public const string ServiceNotes = "serviceNotes"; public const string ExtraServices = "extraServices"; - public const string FlagColor = "flagColor"; public const string RescheduleCount = "RescheduleCount"; private static readonly HashSet DispatchFields = new(StringComparer.OrdinalIgnoreCase) @@ -45,8 +44,7 @@ namespace SeaHaven.Services.Helpers DocStatus, Pm, ServiceNotes, - ExtraServices, - FlagColor + ExtraServices }; public static bool IsEditable(string field) => Canonicalize(field) != null; @@ -86,7 +84,6 @@ namespace SeaHaven.Services.Helpers Pm => "Trade", ServiceNotes => "ServiceNotes", ExtraServices => "ExtraServices", - FlagColor => "FlagColor", _ => canonical }; } diff --git a/SeaHaven.Services/Helpers/WorkOrderFlagColors.cs b/SeaHaven.Services/Helpers/WorkOrderFlagColors.cs deleted file mode 100644 index 6892bd8..0000000 --- a/SeaHaven.Services/Helpers/WorkOrderFlagColors.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace SeaHaven.Services.Helpers -{ - /// - /// Fixed board flag palette (must match FE FLAG_COLORS). Values stored as canonical #RRGGBB. - /// - public static class WorkOrderFlagColors - { - public static readonly HashSet Allowed = new(StringComparer.OrdinalIgnoreCase) - { - "#EF4444", - "#F97316", - "#F59E0B", - "#EAB308", - "#22C55E", - "#14B8A6", - "#3B82F6", - "#8B5CF6" - }; - - /// - /// Normalizes to # + 6 uppercase hex digits, or null if empty. - /// Returns false if the value is non-empty but not a valid hex color. - /// - public static bool TryNormalize(string? value, out string? canonical) - { - canonical = null; - if (string.IsNullOrWhiteSpace(value)) - return true; - - var trimmed = value.Trim(); - if (trimmed.Length == 6 && trimmed[0] != '#') - trimmed = "#" + trimmed; - - if (trimmed.Length != 7 || trimmed[0] != '#') - return false; - - for (var i = 1; i < 7; i++) - { - if (!Char.IsAsciiHexDigit(trimmed[i])) - return false; - } - - canonical = "#" + trimmed[1..].ToUpperInvariant(); - return true; - } - - public static bool IsAllowed(string? canonical) - => canonical != null && Allowed.Contains(canonical); - } -} diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs index 56c576e..076f090 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs @@ -118,7 +118,6 @@ namespace SeaHaven.Services.Implementation ServiceNotes = row.ServiceNotes, ExtraServices = ParseExtraServices(row.ExtraServices), DocStatus = row.DocStatus, - FlagColor = row.FlagColor, PrimaryDispatchId = row.PrimaryDispatchId, RowVersion = row.RowVersion, DispatchRowVersion = row.DispatchRowVersion diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs index c924009..90b57fb 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs @@ -161,7 +161,6 @@ namespace SeaHaven.Services.Implementation WorkOrderBoardFieldNames.Pm => new List { ApplyStringField(value, auditField, v => workOrder.Trade = v, () => workOrder.Trade) }, WorkOrderBoardFieldNames.ServiceNotes => new List { ApplyServiceNotes(workOrder, value, auditField) }, WorkOrderBoardFieldNames.ExtraServices => new List { ApplyExtraServices(workOrder, value, auditField) }, - WorkOrderBoardFieldNames.FlagColor => new List { ApplyFlagColor(workOrder, value, auditField) }, _ => throw new WorkOrderBoardValidationException("InvalidField", $"Field '{field}' is not editable.") }; } @@ -549,24 +548,6 @@ namespace SeaHaven.Services.Implementation return FieldChange.ForField(auditField, old, parsed.ToString()); } - private static FieldChange ApplyFlagColor(WorkOrder workOrder, string? value, string auditField) - { - if (!WorkOrderFlagColors.TryNormalize(value, out var canonical)) - throw new WorkOrderBoardValidationException("InvalidValue", $"Invalid flagColor: {value}"); - - if (canonical != null && !WorkOrderFlagColors.IsAllowed(canonical)) - throw new WorkOrderBoardValidationException( - "InvalidValue", - $"flagColor must be one of the allowed palette values or empty to clear."); - - var old = workOrder.FlagColor; - if (string.Equals(old, canonical, StringComparison.OrdinalIgnoreCase)) - return FieldChange.Unchanged(auditField); - - workOrder.FlagColor = canonical; - return FieldChange.ForField(auditField, old, canonical); - } - private static List ApplyAutoScheduleSideEffects(WorkOrder workOrder) { var changes = new List(); diff --git a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs index 849c5f6..0f7b5d0 100644 --- a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs @@ -114,7 +114,6 @@ namespace SeaHaven.Services.Implementation ServiceNotes = extended?.ServiceNotes ?? row.ServiceNotes, ExtraServices = ParseExtraServices(extended?.ExtraServices) ?? row.ExtraServices, DocStatus = row.DocStatus, - FlagColor = row.FlagColor, PrimaryDispatchId = row.PrimaryDispatchId, RowVersion = row.RowVersion, DispatchRowVersion = row.DispatchRowVersion, diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs index ecaba65..94ba33d 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs @@ -350,157 +350,4 @@ public class WorkOrderBoardUpdateServiceTests Assert.Equal("InvalidValue", ex.Code); } - - [Fact] - public async Task PatchField_SetsFlagColor_CanonicalUppercase() - { - var (context, service) = CreateSut(); - var wo = new WorkOrder - { - Id = 1, - LifecycleStatus = LifecycleStatus.Incomplete, - RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } - }; - context.workOrders.Add(wo); - await context.SaveChangesAsync(); - - var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto - { - Field = WorkOrderBoardFieldNames.FlagColor, - Value = "#ef4444", - WorkOrderVersion = ToVersion(wo) - }, "actor-1"); - - Assert.Equal("#EF4444", result.FlagColor); - Assert.Equal("#EF4444", (await context.workOrders.FindAsync(1))!.FlagColor); - } - - [Fact] - public async Task PatchField_ClearsFlagColor_WithNullOrEmpty() - { - var (context, service) = CreateSut(); - var wo = new WorkOrder - { - Id = 1, - LifecycleStatus = LifecycleStatus.Incomplete, - FlagColor = "#EF4444", - RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } - }; - context.workOrders.Add(wo); - await context.SaveChangesAsync(); - - var cleared = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto - { - Field = WorkOrderBoardFieldNames.FlagColor, - Value = "", - WorkOrderVersion = ToVersion(wo) - }, "actor-1"); - - Assert.Null(cleared.FlagColor); - Assert.Null((await context.workOrders.FindAsync(1))!.FlagColor); - } - - [Fact] - public async Task PatchField_ClearsFlagColor_WithNullValue() - { - var (context, service) = CreateSut(); - var wo = new WorkOrder - { - Id = 1, - LifecycleStatus = LifecycleStatus.Incomplete, - FlagColor = "#3B82F6", - RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } - }; - context.workOrders.Add(wo); - await context.SaveChangesAsync(); - - var cleared = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto - { - Field = WorkOrderBoardFieldNames.FlagColor, - Value = null, - WorkOrderVersion = ToVersion(wo) - }, "actor-1"); - - Assert.Null(cleared.FlagColor); - Assert.Null((await context.workOrders.FindAsync(1))!.FlagColor); - } - - [Fact] - public async Task PatchField_FlagColorOutsidePalette_Throws() - { - var (context, service) = CreateSut(); - var wo = new WorkOrder - { - Id = 1, - LifecycleStatus = LifecycleStatus.Incomplete, - RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } - }; - context.workOrders.Add(wo); - await context.SaveChangesAsync(); - - var ex = await Assert.ThrowsAsync(() => - service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto - { - Field = WorkOrderBoardFieldNames.FlagColor, - Value = "#000000", - WorkOrderVersion = ToVersion(wo) - }, "actor-1")); - - Assert.Equal("InvalidValue", ex.Code); - } - - [Fact] - public async Task PatchField_FlagColorMalformed_Throws() - { - var (context, service) = CreateSut(); - var wo = new WorkOrder - { - Id = 1, - LifecycleStatus = LifecycleStatus.Incomplete, - RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } - }; - context.workOrders.Add(wo); - await context.SaveChangesAsync(); - - var ex = await Assert.ThrowsAsync(() => - service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto - { - Field = WorkOrderBoardFieldNames.FlagColor, - Value = "not-a-color", - WorkOrderVersion = ToVersion(wo) - }, "actor-1")); - - Assert.Equal("InvalidValue", ex.Code); - } - - [Fact] - public async Task PatchField_FlagColor_DoesNotRequireDispatchVersion() - { - var (context, service) = CreateSut(); - var wo = new WorkOrder - { - Id = 1, - LifecycleStatus = LifecycleStatus.Incomplete, - PrimaryDispatchId = 10, - RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } - }; - context.Dispatches.Add(new Dispatch - { - Id = 10, - WorkOrderId = 1, - RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } - }); - context.workOrders.Add(wo); - await context.SaveChangesAsync(); - - var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto - { - Field = WorkOrderBoardFieldNames.FlagColor, - Value = "#8B5CF6", - WorkOrderVersion = ToVersion(wo) - // no DispatchVersion - }, "actor-1"); - - Assert.Equal("#8B5CF6", result.FlagColor); - } }