From 15315edf083f6e815ebda79d359b7ae376b5f450 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Mon, 24 Aug 2026 15:14:31 -0300 Subject: [PATCH] feat(work-orders): persist avetaRequired on board create, patch, and search Expose avetaRequired and originalDate on list rows so the frontend can round-trip the Aveta checkbox and Reschedule hover. --- .../20260824150000_SH117_AvetaRequired.cs | 33 ++++++++ .../ApplicationDbContextModelSnapshot.cs | 3 + Data.SeaHavenIndustries/Models/WorkerOrder.cs | 4 + .../Helpers/WorkOrderBoardProjection.cs | 6 ++ .../WorkOrderBoardSearchFacetFilters.cs | 10 +++ .../WorkOrderAdvancedSearchDataService.cs | 1 + .../WorkOrderAdvancedSearchModels.cs | 1 + .../Interfaces/WorkOrderBoardModels.cs | 3 + SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs | 3 + .../DTOs/WorkOrderBoardRequestDTOs.cs | 4 + SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs | 2 - .../Helpers/WorkOrderBoardFieldNames.cs | 5 +- .../WorkOrderAdvancedSearchService.cs | 1 + .../WorkOrderBoardCreateService.cs | 3 + .../Implementation/WorkOrderBoardService.cs | 3 + .../WorkOrderBoardUpdateService.cs | 10 +++ .../Implementation/WorkOrderDetailService.cs | 5 +- .../WorkOrderBoardCreateServiceTests.cs | 39 ++++++++++ .../WorkOrderBoardSearchTests.cs | 47 ++++++++++++ .../WorkOrderBoardServiceTests.cs | 34 +++++++++ .../WorkOrderBoardUpdateServiceTests.cs | 76 +++++++++++++++++++ .../WorkOrderMigrationDiscoveryTests.cs | 13 ++++ 22 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 Data.SeaHavenIndustries/Migrations/20260824150000_SH117_AvetaRequired.cs diff --git a/Data.SeaHavenIndustries/Migrations/20260824150000_SH117_AvetaRequired.cs b/Data.SeaHavenIndustries/Migrations/20260824150000_SH117_AvetaRequired.cs new file mode 100644 index 0000000..eb71969 --- /dev/null +++ b/Data.SeaHavenIndustries/Migrations/20260824150000_SH117_AvetaRequired.cs @@ -0,0 +1,33 @@ +using Data.SeaHavenIndustries; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.SeaHavenIndustries.Migrations +{ + /// + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260824150000_SH117_AvetaRequired")] + public partial class SH117_AvetaRequired : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AvetaRequired", + table: "workOrders", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AvetaRequired", + table: "workOrders"); + } + } +} diff --git a/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs b/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs index 0efc712..a3051ed 100644 --- a/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Data.SeaHavenIndustries/Migrations/ApplicationDbContextModelSnapshot.cs @@ -2410,6 +2410,9 @@ namespace Data.SeaHavenIndustries.Migrations b.Property("Attachments") .HasColumnType("nvarchar(max)"); + b.Property("AvetaRequired") + .HasColumnType("bit"); + b.Property("AvettaTask") .HasColumnType("nvarchar(max)"); diff --git a/Data.SeaHavenIndustries/Models/WorkerOrder.cs b/Data.SeaHavenIndustries/Models/WorkerOrder.cs index 733b01e..67549ad 100644 --- a/Data.SeaHavenIndustries/Models/WorkerOrder.cs +++ b/Data.SeaHavenIndustries/Models/WorkerOrder.cs @@ -62,6 +62,10 @@ namespace Data.SeaHavenIndustries public string? ExtraServices { get; set; } [MaxLength(4000)] public string? ServiceNotes { get; set; } + /// + /// Per-WO Aveta compliance flag (SH-117). Distinct from legacy AvettaTask string. + /// + public bool AvetaRequired { get; set; } /// Tech/crew phone when no primary dispatch; otherwise prefer Dispatch.TechPhone. [MaxLength(50)] public string? TechPhone { get; set; } diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs index 480d534..0ccb890 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs @@ -19,6 +19,9 @@ namespace SeaHaven.DataServices.Helpers w.RescheduleCount, w.CarriedOver, w.IsAddOn, + w.AvetaRequired, + w.OriginalDate, + w.OriginalWeek, w.WorkOrderType, w.SiteCode, LocationName = w.Locations != null ? w.Locations.Name : null, @@ -176,6 +179,9 @@ namespace SeaHaven.DataServices.Helpers w.PrimaryUpliftStatus, w.PrimaryUpliftAmount, w.PrimaryDispatchStatus, + w.AvetaRequired, + w.OriginalDate, + w.OriginalWeek, isUnscheduled); }).ToList(); } diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFacetFilters.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFacetFilters.cs index 08b2c2d..d3bdfea 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFacetFilters.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFacetFilters.cs @@ -74,6 +74,16 @@ namespace SeaHaven.DataServices.Helpers return query.Where(w => w.IsAddOn); } + public static IQueryable ApplyAvetaOnlyFilter( + IQueryable query, + bool avetaOnly) + { + if (!avetaOnly) + return query; + + return query.Where(w => w.AvetaRequired); + } + public static IQueryable ApplyFlagColorFilter( IQueryable query, IReadOnlyList? flagColors) diff --git a/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs b/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs index 1d4892d..2eebc30 100644 --- a/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs +++ b/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs @@ -32,6 +32,7 @@ namespace SeaHaven.DataServices.Implementation baseQuery = WorkOrderBoardSearchFacetFilters.ApplyRescheduledFilter(baseQuery, query.Rescheduled); baseQuery = WorkOrderBoardSearchFacetFilters.ApplyCarriedOverFilter(baseQuery, query.CarriedOver); baseQuery = WorkOrderBoardSearchFacetFilters.ApplyAddOnFilter(baseQuery, query.AddOn); + baseQuery = WorkOrderBoardSearchFacetFilters.ApplyAvetaOnlyFilter(baseQuery, query.AvetaOnly); baseQuery = WorkOrderBoardSearchFacetFilters.ApplyFlagColorFilter(baseQuery, query.FlagColors); baseQuery = WorkOrderBoardSearchFacetFilters.ApplyInternalOnlyFilter(baseQuery, query.InternalOnly); baseQuery = WorkOrderBoardSearchFacetFilters.ApplyUpliftFilter( diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs index b965e86..44889dc 100644 --- a/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs +++ b/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs @@ -19,6 +19,7 @@ namespace SeaHaven.DataServices.Interfaces bool Rescheduled, bool CarriedOver, bool AddOn, + bool AvetaOnly, IReadOnlyList? FlagColors, bool InternalOnly, bool HasUplift, diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs index 6ae2928..d4cb09c 100644 --- a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs +++ b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs @@ -59,6 +59,9 @@ namespace SeaHaven.DataServices.Interfaces string? PrimaryUpliftStatus, decimal? PrimaryUpliftAmount, string? PrimaryDispatchStatus, + bool AvetaRequired, + DateOnly? OriginalDate, + DateOnly? OriginalWeek, bool IsUnscheduled); public record WorkOrderBoardQueryResult( diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs index f7ecc1d..395e858 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs @@ -36,6 +36,9 @@ namespace SeaHaven.Services.DTOs public int RescheduleCount { get; set; } public int CarriedOver { get; set; } public bool IsAddOn { get; set; } + public bool AvetaRequired { get; set; } + public DateOnly? OriginalDate { get; set; } + public DateOnly? OriginalWeek { get; set; } public WorkOrderType? WorkOrderType { get; set; } public bool IsPastDue { get; set; } public string? SiteCode { get; set; } diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs index 9a5de5d..d9718ff 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs @@ -32,6 +32,8 @@ namespace SeaHaven.Services.DTOs public string? PrimaryService { get; set; } public List? ExtraServices { get; set; } public string? ServiceNotes { get; set; } + /// Per-WO Aveta compliance. Omit or false = not required. + public bool? AvetaRequired { get; set; } public int? LocationId { get; set; } public DateTime? DueDate { get; set; } public string? AssignTo { get; set; } @@ -73,6 +75,8 @@ namespace SeaHaven.Services.DTOs public bool Rescheduled { get; set; } public bool CarriedOver { get; set; } public bool AddOn { get; set; } + /// When true, only rows with AvetaRequired. + public bool AvetaOnly { get; set; } public List? FlagColors { get; set; } public bool InternalOnly { get; set; } public bool HasUplift { get; set; } diff --git a/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs index 8f2c692..9832b01 100644 --- a/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs @@ -17,8 +17,6 @@ namespace SeaHaven.Services.DTOs public string? Trade { get; set; } public string? Problem { get; set; } public string? SubTrade { get; set; } - public DateOnly? OriginalWeek { get; set; } - public DateOnly? OriginalDate { get; set; } public string? VendorNotes { get; set; } } diff --git a/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs b/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs index 45ae58f..4056197 100644 --- a/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs +++ b/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs @@ -21,6 +21,7 @@ namespace SeaHaven.Services.Helpers public const string ExtraServices = "extraServices"; public const string AdditionalContacts = "additionalContacts"; public const string FlagColor = "flagColor"; + public const string AvetaRequired = "avetaRequired"; public const string RescheduleCount = "RescheduleCount"; private static readonly HashSet DispatchFields = new(StringComparer.OrdinalIgnoreCase) @@ -50,7 +51,8 @@ namespace SeaHaven.Services.Helpers ServiceNotes, ExtraServices, AdditionalContacts, - FlagColor + FlagColor, + AvetaRequired }; public static bool IsEditable(string field) => Canonicalize(field) != null; @@ -93,6 +95,7 @@ namespace SeaHaven.Services.Helpers ExtraServices => "ExtraServices", AdditionalContacts => "AdditionalContacts", FlagColor => "FlagColor", + AvetaRequired => "AvetaRequired", _ => canonical }; } diff --git a/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs b/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs index f1e79cf..6185f99 100644 --- a/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs @@ -55,6 +55,7 @@ namespace SeaHaven.Services.Implementation query.Rescheduled, query.CarriedOver, query.AddOn, + query.AvetaOnly, query.FlagColors, query.InternalOnly, query.HasUplift, diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs index a5df677..f644f6e 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs @@ -85,6 +85,7 @@ namespace SeaHaven.Services.Implementation Trade = primaryService, ExtraServices = extraServicesJson, ServiceNotes = serviceNotes, + AvetaRequired = request.AvetaRequired == true, PocName = pocName, PocPhone = pocPhone, PocNotes = pocNotes, @@ -130,6 +131,8 @@ namespace SeaHaven.Services.Implementation changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ExtraServices", "", workOrder.ExtraServices)); if (workOrder.ServiceNotes != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ServiceNotes", "", workOrder.ServiceNotes)); + if (workOrder.AvetaRequired) + changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("AvetaRequired", "", bool.TrueString)); if (workOrder.PocName != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocName", "", workOrder.PocName)); if (workOrder.PocPhone != null) diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs index c1b2f42..c4405b3 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs @@ -99,6 +99,9 @@ namespace SeaHaven.Services.Implementation RescheduleCount = row.RescheduleCount, CarriedOver = row.CarriedOver, IsAddOn = row.IsAddOn, + AvetaRequired = row.AvetaRequired, + OriginalDate = row.OriginalDate, + OriginalWeek = row.OriginalWeek, WorkOrderType = row.WorkOrderType, IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow), SiteCode = row.SiteCode, diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs index 5a11b21..6c27693 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs @@ -193,6 +193,7 @@ namespace SeaHaven.Services.Implementation WorkOrderBoardFieldNames.ExtraServices => new List { ApplyExtraServices(workOrder, value, auditField) }, WorkOrderBoardFieldNames.AdditionalContacts => new List { ApplyAdditionalContacts(workOrder, value, auditField) }, WorkOrderBoardFieldNames.FlagColor => new List { ApplyFlagColor(workOrder, value, auditField) }, + WorkOrderBoardFieldNames.AvetaRequired => new List { ApplyAvetaRequired(workOrder, value, auditField) }, _ => throw new WorkOrderBoardValidationException("InvalidField", $"Field '{field}' is not editable.") }; } @@ -679,6 +680,15 @@ namespace SeaHaven.Services.Implementation return FieldChange.ForField(auditField, old, parsed.ToString()); } + private static FieldChange ApplyAvetaRequired(WorkOrder workOrder, string? value, string auditField) + { + return ApplyBoolField( + value, + auditField, + v => workOrder.AvetaRequired = v == true, + () => workOrder.AvetaRequired); + } + private static FieldChange ApplyFlagColor(WorkOrder workOrder, string? value, string auditField) { if (!WorkOrderFlagColors.TryNormalize(value, out var canonical)) diff --git a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs index 9fcf0ab..b30f052 100644 --- a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs @@ -96,6 +96,7 @@ namespace SeaHaven.Services.Implementation RescheduleCount = row.RescheduleCount, CarriedOver = row.CarriedOver, IsAddOn = row.IsAddOn, + AvetaRequired = row.AvetaRequired, WorkOrderType = row.WorkOrderType, IsPastDue = row.IsPastDue, SiteCode = row.SiteCode, @@ -137,8 +138,8 @@ namespace SeaHaven.Services.Implementation Trade = extended?.Trade, Problem = extended?.Problem, SubTrade = extended?.SubTrade, - OriginalWeek = extended?.OriginalWeek, - OriginalDate = extended?.OriginalDate, + OriginalWeek = row.OriginalWeek ?? extended?.OriginalWeek, + OriginalDate = row.OriginalDate ?? extended?.OriginalDate, CompletedDate = extended?.CompletedDate, VendorNotes = extended?.VendorNotes }; diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs index d3df3a9..53f4f84 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs @@ -432,6 +432,45 @@ public class WorkOrderBoardCreateServiceTests Assert.True(wo.IsAddOn); } + [Fact] + public async Task Create_AvetaRequiredTrue_PersistsAndReturns() + { + var (context, service) = CreateSut(); + + var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto + { + WorkOrderType = WorkOrderType.PM, + SiteCode = "BK5", + AvetaRequired = true + }, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"); + + Assert.True(result.AvetaRequired); + var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id); + Assert.True(wo.AvetaRequired); + Assert.Contains( + await context.WorkOrderAuditLogs.ToListAsync(), + l => l.FieldName == "AvetaRequired" && l.Action == "FieldChanged"); + } + + [Fact] + public async Task Create_AvetaRequiredOmitted_DefaultsFalse() + { + var (context, service) = CreateSut(); + + var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto + { + WorkOrderType = WorkOrderType.PM, + SiteCode = "BK5" + }, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"); + + Assert.False(result.AvetaRequired); + var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id); + Assert.False(wo.AvetaRequired); + Assert.DoesNotContain( + await context.WorkOrderAuditLogs.ToListAsync(), + l => l.FieldName == "AvetaRequired"); + } + [Fact] public async Task Create_WorkOrderTypeAddOn_ThrowsValidation() { diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs index 9da4328..7f98f9a 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs @@ -829,4 +829,51 @@ public class WorkOrderAdvancedSearchServiceTests Assert.Equal(1, result.TotalCount); Assert.Equal(1, result.Items.Single().Id); } + + [Fact] + public async Task SearchAsync_AvetaOnly_ReturnsFlaggedRows() + { + await using var context = CreateContext(); + var weekStart = new DateOnly(2026, 6, 22); + + context.workOrders.AddRange( + new WorkOrder + { + Id = 1, + AvetaRequired = true, + ScheduledDate = new DateTime(2026, 6, 23), + LifecycleStatus = LifecycleStatus.Scheduled, + InternalWONumber = "10000000001", + }, + new WorkOrder + { + Id = 2, + AvetaRequired = false, + ScheduledDate = new DateTime(2026, 6, 24), + LifecycleStatus = LifecycleStatus.Scheduled, + InternalWONumber = "10000000002", + }); + + await context.SaveChangesAsync(); + + var dataService = new WorkOrderAdvancedSearchDataService(context); + var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService( + dataService, + WorkOrderAccountTestHelpers.Resolver(context)); + + var result = await service.SearchAsync( + new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto + { + DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom, + DateFrom = weekStart, + DateTo = weekStart.AddDays(6), + AvetaOnly = true, + }, + WorkOrderAccountTestHelpers.OrgWideAdmin(), + null); + + Assert.Equal(1, result.TotalCount); + Assert.Equal(1, result.Items.Single().Id); + Assert.True(result.Items.Single().AvetaRequired); + } } diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs index dc536ce..a13bc22 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs @@ -448,6 +448,40 @@ public class WorkOrderBoardServiceTests Assert.Equal(WorkOrderType.PM, response.Scheduled[0].WorkOrderType); } + [Fact] + public async Task GetBoardAsync_MapsAvetaRequiredAndOriginalDate() + { + await using var context = CreateContext(); + var weekStart = new DateOnly(2026, 6, 22); + + context.workOrders.Add(new WorkOrder + { + Id = 1, + InternalWONumber = "10000000001", + WorkOrderType = WorkOrderType.PM, + AvetaRequired = true, + OriginalDate = new DateOnly(2026, 5, 8), + OriginalWeek = new DateOnly(2026, 5, 4), + ScheduledDate = new DateTime(2026, 6, 24, 12, 0, 0, DateTimeKind.Utc), + LifecycleStatus = LifecycleStatus.Scheduled + }); + + await context.SaveChangesAsync(); + + var boardService = new WorkOrderBoardService( + new WorkOrderBoardDataService(context), + WorkOrderAccountTestHelpers.Resolver(context)); + var response = await boardService.GetBoardAsync(new WorkOrderBoardQueryDto + { + WeekStart = weekStart + }, WorkOrderAccountTestHelpers.OrgWideAdmin(), null); + + Assert.Single(response.Scheduled); + Assert.True(response.Scheduled[0].AvetaRequired); + Assert.Equal(new DateOnly(2026, 5, 8), response.Scheduled[0].OriginalDate); + Assert.Equal(new DateOnly(2026, 5, 4), response.Scheduled[0].OriginalWeek); + } + [Fact] public async Task GetBoardAsync_InvalidWeekWindow_Throws() { diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs index ebf9996..9bdf934 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs @@ -1388,4 +1388,80 @@ public class WorkOrderBoardUpdateServiceTests Assert.True(persisted.IsAddOn); Assert.Empty(await context.WorkOrderAuditLogs.Where(a => a.FieldName == "IsAddOn").ToListAsync()); } + + [Fact] + public async Task PatchField_AvetaRequiredTrue_PersistsAndAudits() + { + 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.AvetaRequired, + Value = "true", + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); + + Assert.True(result.AvetaRequired); + Assert.True((await context.workOrders.FindAsync(1))!.AvetaRequired); + Assert.Contains( + await context.WorkOrderAuditLogs.ToListAsync(), + a => a.FieldName == "AvetaRequired" && a.Action == AuditActionType.FieldChanged.ToString()); + } + + [Fact] + public async Task PatchField_AvetaRequiredFalse_ClearsFlag() + { + var (context, service) = CreateSut(); + var wo = new WorkOrder + { + Id = 1, + LifecycleStatus = LifecycleStatus.Incomplete, + AvetaRequired = true, + 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.AvetaRequired, + Value = "false", + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); + + Assert.False(result.AvetaRequired); + Assert.False((await context.workOrders.FindAsync(1))!.AvetaRequired); + } + + [Fact] + public async Task PatchField_AvetaRequiredInvalid_ThrowsInvalidValue() + { + 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.AvetaRequired, + Value = "yes", + WorkOrderVersion = ToVersion(wo) + }, "actor-1")); + + Assert.Equal("InvalidValue", ex.Code); + } } diff --git a/SeaHavenIndustries.Tests/WorkOrderMigrationDiscoveryTests.cs b/SeaHavenIndustries.Tests/WorkOrderMigrationDiscoveryTests.cs index 5484926..8e34d94 100644 --- a/SeaHavenIndustries.Tests/WorkOrderMigrationDiscoveryTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderMigrationDiscoveryTests.cs @@ -19,4 +19,17 @@ public class WorkOrderMigrationDiscoveryTests Assert.Contains("20260713120000_Phase7_ExternalWorkOrderIdUnique", migrations); } + + [Fact] + public void SH117_AvetaRequired_is_discoverable_by_ef_runtime() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("DataSource=:memory:") + .Options; + + using var context = new ApplicationDbContext(options); + var migrations = context.Database.GetMigrations().ToList(); + + Assert.Contains("20260824150000_SH117_AvetaRequired", migrations); + } }