diff --git a/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs b/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs
index b8db7fc..f50824f 100644
--- a/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs
+++ b/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs
@@ -165,7 +165,8 @@ namespace Data.SeaHavenIndustries.Enums
Before = 1,
After = 2,
Extra = 3,
- Completion = 4
+ Completion = 4,
+ Aveta = 5
}
}
\ No newline at end of file
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..5e87a2e 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. 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 9e592e6..aaf1778 100644
--- a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs
+++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs
@@ -1,4 +1,5 @@
using Data.SeaHavenIndustries;
+using Data.SeaHavenIndustries.Enums;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
@@ -20,6 +21,12 @@ namespace SeaHaven.DataServices.Helpers
w.RescheduleCount,
w.CarriedOver,
w.IsAddOn,
+ w.AvetaRequired,
+ HasAvetaDocument = w.workOrderAttachments!.Any(a =>
+ (a.IsDeleted == null || a.IsDeleted == false)
+ && a.Category == WorkOrderMediaCategory.Aveta),
+ w.OriginalDate,
+ w.OriginalWeek,
w.WorkOrderType,
w.SiteCode,
LocationName = w.Locations != null ? w.Locations.Name : null,
@@ -144,6 +151,10 @@ namespace SeaHaven.DataServices.Helpers
uplift?.PrimaryStatus,
uplift?.PrimaryAmount,
w.PrimaryDispatchStatus,
+ w.AvetaRequired,
+ w.HasAvetaDocument,
+ 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..da84f94 100644
--- a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs
+++ b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs
@@ -59,6 +59,10 @@ namespace SeaHaven.DataServices.Interfaces
string? PrimaryUpliftStatus,
decimal? PrimaryUpliftAmount,
string? PrimaryDispatchStatus,
+ bool AvetaRequired,
+ bool HasAvetaDocument,
+ DateOnly? OriginalDate,
+ DateOnly? OriginalWeek,
bool IsUnscheduled);
public record WorkOrderBoardQueryResult(
diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs
index d26295c..c4704ea 100644
--- a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs
+++ b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs
@@ -41,6 +41,10 @@ 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 bool HasAvetaDocument { 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/Helpers/WorkOrderBoardMutationRules.cs b/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs
index 7a52474..dde4bfb 100644
--- a/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs
+++ b/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs
@@ -33,7 +33,13 @@ namespace SeaHaven.Services.Helpers
return false;
if (!IsCoreLocked(status))
return true;
- return (category ?? WorkOrderMediaCategory.Extra) == WorkOrderMediaCategory.Extra;
+ return IsExtraDocsFamily(category);
+ }
+
+ public static bool IsExtraDocsFamily(WorkOrderMediaCategory? category)
+ {
+ var resolved = category ?? WorkOrderMediaCategory.Extra;
+ return resolved == WorkOrderMediaCategory.Extra || resolved == WorkOrderMediaCategory.Aveta;
}
public static bool ShouldBlockStatusChangeWhenPastDue(string field, bool isPastDue)
diff --git a/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs b/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs
index 888a718..071acf0 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 1f03160..972e2f1 100644
--- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs
+++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs
@@ -100,6 +100,10 @@ namespace SeaHaven.Services.Implementation
RescheduleCount = row.RescheduleCount,
CarriedOver = row.CarriedOver,
IsAddOn = row.IsAddOn,
+ AvetaRequired = row.AvetaRequired,
+ HasAvetaDocument = row.HasAvetaDocument,
+ 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..7f4d132 100644
--- a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs
+++ b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs
@@ -96,6 +96,8 @@ namespace SeaHaven.Services.Implementation
RescheduleCount = row.RescheduleCount,
CarriedOver = row.CarriedOver,
IsAddOn = row.IsAddOn,
+ AvetaRequired = row.AvetaRequired,
+ HasAvetaDocument = row.HasAvetaDocument,
WorkOrderType = row.WorkOrderType,
IsPastDue = row.IsPastDue,
SiteCode = row.SiteCode,
@@ -137,8 +139,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/WorkOrderBoardMutationRulesTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs
index 195c8d1..6494004 100644
--- a/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs
@@ -87,8 +87,12 @@ public class WorkOrderBoardMutationRulesTests
LifecycleStatus.Completed, WorkOrderMediaCategory.Before));
Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
LifecycleStatus.Completed, WorkOrderMediaCategory.After));
+ Assert.True(WorkOrderBoardMutationRules.CanMutateExtraMedia(
+ LifecycleStatus.Completed, WorkOrderMediaCategory.Aveta));
Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
LifecycleStatus.Canceled, WorkOrderMediaCategory.Extra));
+ Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
+ LifecycleStatus.Canceled, WorkOrderMediaCategory.Aveta));
}
[Fact]
diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs
index fe3d37d..9c8dd75 100644
--- a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs
@@ -1001,4 +1001,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 b9fe3a4..123cc03 100644
--- a/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderBoardServiceTests.cs
@@ -724,6 +724,111 @@ 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);
+ Assert.False(response.Scheduled[0].HasAvetaDocument);
+ }
+
+ [Fact]
+ public async Task GetBoardAsync_HasAvetaDocument_WhenAvetaAttachmentExists()
+ {
+ await using var context = CreateContext();
+ var weekStart = new DateOnly(2026, 6, 22);
+
+ context.workOrders.Add(new WorkOrder
+ {
+ Id = 1,
+ InternalWONumber = "10000000001",
+ AvetaRequired = true,
+ ScheduledDate = new DateTime(2026, 6, 24, 12, 0, 0, DateTimeKind.Utc),
+ LifecycleStatus = LifecycleStatus.Scheduled
+ });
+ context.workOrderAttachments.Add(new WorkOrderAttachments
+ {
+ WorkorderId = 1,
+ Attachments = "https://example.com/aveta.pdf",
+ Category = WorkOrderMediaCategory.Aveta
+ });
+ 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.True(response.Scheduled[0].HasAvetaDocument);
+ }
+
+ [Fact]
+ public async Task GetBoardAsync_HasAvetaDocumentFalse_WhenOnlyExtraAttachment()
+ {
+ await using var context = CreateContext();
+ var weekStart = new DateOnly(2026, 6, 22);
+
+ context.workOrders.Add(new WorkOrder
+ {
+ Id = 1,
+ InternalWONumber = "10000000001",
+ AvetaRequired = true,
+ ScheduledDate = new DateTime(2026, 6, 24, 12, 0, 0, DateTimeKind.Utc),
+ LifecycleStatus = LifecycleStatus.Scheduled
+ });
+ context.workOrderAttachments.Add(new WorkOrderAttachments
+ {
+ WorkorderId = 1,
+ Attachments = "https://example.com/extra.pdf",
+ Category = WorkOrderMediaCategory.Extra
+ });
+ 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.False(response.Scheduled[0].HasAvetaDocument);
+ }
+
[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/WorkOrderCompletedSelectiveLockTests.cs b/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs
index 9ebd221..733c589 100644
--- a/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs
@@ -242,6 +242,24 @@ public class WorkOrderCompletedSelectiveLockTests
Assert.Equal(WorkOrderMediaCategory.Extra, media.Category);
}
+ [Fact]
+ public async Task AddMedia_Completed_AllowsAveta()
+ {
+ var (context, service) = CreateMediaSut();
+ context.workOrders.Add(CompletedWorkOrder());
+ await context.SaveChangesAsync();
+
+ var media = await service.AddMediaAsync(
+ 1,
+ WorkOrderMediaCategory.Aveta,
+ "https://example.com/aveta.pdf",
+ WorkOrderAccountTestHelpers.OrgWideAdmin(),
+ "actor-1");
+
+ Assert.True(media.Id > 0);
+ Assert.Equal(WorkOrderMediaCategory.Aveta, media.Category);
+ }
+
[Fact]
public async Task AddMedia_Completed_BlocksBefore()
{
@@ -286,6 +304,32 @@ public class WorkOrderCompletedSelectiveLockTests
Assert.Equal(WorkOrderMediaCategory.Extra, media.Category);
}
+ [Fact]
+ public async Task UpdateMediaCategory_Completed_AllowsAveta()
+ {
+ var (context, service) = CreateMediaSut();
+ var wo = CompletedWorkOrder();
+ context.workOrders.Add(wo);
+ context.workOrderAttachments.Add(new WorkOrderAttachments
+ {
+ Id = 10,
+ WorkorderId = 1,
+ Attachments = "https://example.com/extra.pdf",
+ Category = WorkOrderMediaCategory.Extra
+ });
+ await context.SaveChangesAsync();
+
+ var media = await service.UpdateMediaCategoryAsync(
+ 1,
+ 10,
+ WorkOrderMediaCategory.Aveta,
+ ToVersion(wo),
+ WorkOrderAccountTestHelpers.OrgWideAdmin(),
+ "actor-1");
+
+ Assert.Equal(WorkOrderMediaCategory.Aveta, media.Category);
+ }
+
[Fact]
public async Task UpdateMediaCategory_Completed_BlocksMoveToBefore()
{
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);
+ }
}
diff --git a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs
index 1586a83..7e6a254 100644
--- a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs
@@ -866,6 +866,101 @@ public class WorkOrderMediaServiceTests
Assert.Contains(media, m => m.Category == WorkOrderMediaCategory.Extra);
}
+ [Fact]
+ public async Task GetMedia_IncludesAvetaCategory()
+ {
+ var (context, service) = CreateSut();
+ context.workOrders.Add(new WorkOrder { Id = 1 });
+ context.workOrderAttachments.Add(new WorkOrderAttachments
+ {
+ WorkorderId = 1,
+ Attachments = "https://example.com/aveta.pdf",
+ Category = WorkOrderMediaCategory.Aveta
+ });
+ await context.SaveChangesAsync();
+
+ var media = await service.GetMediaAsync(1, AuthenticatedUser(), "actor-1");
+
+ Assert.NotNull(media);
+ Assert.Contains(media!, m => m.Category == WorkOrderMediaCategory.Aveta);
+ }
+
+ [Fact]
+ public async Task AddMedia_Aveta_PersistsCategory()
+ {
+ var (context, service) = CreateSut();
+ context.workOrders.Add(new WorkOrder
+ {
+ Id = 1,
+ LifecycleStatus = LifecycleStatus.Scheduled,
+ RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
+ });
+ await context.SaveChangesAsync();
+
+ var media = await service.AddMediaAsync(
+ 1,
+ WorkOrderMediaCategory.Aveta,
+ "https://example.com/aveta.pdf",
+ AuthenticatedUser(),
+ "actor-1");
+
+ Assert.True(media.Id > 0);
+ Assert.Equal(WorkOrderMediaCategory.Aveta, media.Category);
+ Assert.Equal(WorkOrderMediaCategory.Aveta, context.workOrderAttachments.Single().Category);
+ }
+
+ [Fact]
+ public async Task AddMedia_Extra_DoesNotPersistAsAveta()
+ {
+ var (context, service) = CreateSut();
+ context.workOrders.Add(new WorkOrder
+ {
+ Id = 1,
+ LifecycleStatus = LifecycleStatus.Scheduled,
+ RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
+ });
+ await context.SaveChangesAsync();
+
+ var media = await service.AddMediaAsync(
+ 1,
+ WorkOrderMediaCategory.Extra,
+ "https://example.com/extra.pdf",
+ AuthenticatedUser(),
+ "actor-1");
+
+ Assert.Equal(WorkOrderMediaCategory.Extra, media.Category);
+ Assert.Equal(WorkOrderMediaCategory.Extra, context.workOrderAttachments.Single().Category);
+ Assert.NotEqual(WorkOrderMediaCategory.Aveta, context.workOrderAttachments.Single().Category);
+ }
+
+ [Fact]
+ public async Task UpdateMediaCategory_ExtraToAveta()
+ {
+ var (context, service) = CreateSut();
+ var wo = new WorkOrder
+ {
+ Id = 1,
+ LifecycleStatus = LifecycleStatus.Scheduled,
+ RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
+ };
+ context.workOrders.Add(wo);
+ context.workOrderAttachments.Add(new WorkOrderAttachments
+ {
+ Id = 10,
+ WorkorderId = 1,
+ Attachments = "https://example.com/extra.pdf",
+ Category = WorkOrderMediaCategory.Extra
+ });
+ await context.SaveChangesAsync();
+
+ var media = await service.UpdateMediaCategoryAsync(
+ 1, 10, WorkOrderMediaCategory.Aveta, ToVersion(wo), AuthenticatedUser(), "actor-1");
+
+ Assert.Equal(WorkOrderMediaCategory.Aveta, media.Category);
+ Assert.Equal(WorkOrderMediaCategory.Aveta, context.workOrderAttachments.Single().Category);
+ Assert.True(context.workOrderAttachments.Single().IsDeleted != true);
+ }
+
[Fact]
public async Task GetMedia_TechnicianOnAssignedWorkOrder_Succeeds()
{