mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
Merge pull request #80 from Sea-Haven-Industries/feat/sh-117-aveta-required
feat(work-orders): persist avetaRequired on board create, patch, and search
This commit is contained in:
commit
df01ded3ce
27 changed files with 533 additions and 7 deletions
|
|
@ -165,7 +165,8 @@ namespace Data.SeaHavenIndustries.Enums
|
|||
Before = 1,
|
||||
After = 2,
|
||||
Extra = 3,
|
||||
Completion = 4
|
||||
Completion = 4,
|
||||
Aveta = 5
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260824150000_SH117_AvetaRequired")]
|
||||
public partial class SH117_AvetaRequired : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AvetaRequired",
|
||||
table: "workOrders",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AvetaRequired",
|
||||
table: "workOrders");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2410,6 +2410,9 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<string>("Attachments")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("AvetaRequired")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("AvettaTask")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ namespace Data.SeaHavenIndustries
|
|||
public string? ExtraServices { get; set; }
|
||||
[MaxLength(4000)]
|
||||
public string? ServiceNotes { get; set; }
|
||||
/// <summary>
|
||||
/// Per-WO Aveta compliance flag. Distinct from legacy AvettaTask string.
|
||||
/// </summary>
|
||||
public bool AvetaRequired { get; set; }
|
||||
/// <summary>Tech/crew phone when no primary dispatch; otherwise prefer Dispatch.TechPhone.</summary>
|
||||
[MaxLength(50)]
|
||||
public string? TechPhone { get; set; }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,16 @@ namespace SeaHaven.DataServices.Helpers
|
|||
return query.Where(w => w.IsAddOn);
|
||||
}
|
||||
|
||||
public static IQueryable<WorkOrder> ApplyAvetaOnlyFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
bool avetaOnly)
|
||||
{
|
||||
if (!avetaOnly)
|
||||
return query;
|
||||
|
||||
return query.Where(w => w.AvetaRequired);
|
||||
}
|
||||
|
||||
public static IQueryable<WorkOrder> ApplyFlagColorFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<string>? flagColors)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
bool Rescheduled,
|
||||
bool CarriedOver,
|
||||
bool AddOn,
|
||||
bool AvetaOnly,
|
||||
IReadOnlyList<string>? FlagColors,
|
||||
bool InternalOnly,
|
||||
bool HasUplift,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ namespace SeaHaven.Services.DTOs
|
|||
public string? PrimaryService { get; set; }
|
||||
public List<string>? ExtraServices { get; set; }
|
||||
public string? ServiceNotes { get; set; }
|
||||
/// <summary>Per-WO Aveta compliance. Omit or false = not required.</summary>
|
||||
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; }
|
||||
/// <summary>When true, only rows with AvetaRequired.</summary>
|
||||
public bool AvetaOnly { get; set; }
|
||||
public List<string>? FlagColors { get; set; }
|
||||
public bool InternalOnly { get; set; }
|
||||
public bool HasUplift { get; set; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string> 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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ namespace SeaHaven.Services.Implementation
|
|||
query.Rescheduled,
|
||||
query.CarriedOver,
|
||||
query.AddOn,
|
||||
query.AvetaOnly,
|
||||
query.FlagColors,
|
||||
query.InternalOnly,
|
||||
query.HasUplift,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ namespace SeaHaven.Services.Implementation
|
|||
WorkOrderBoardFieldNames.ExtraServices => new List<FieldChange> { ApplyExtraServices(workOrder, value, auditField) },
|
||||
WorkOrderBoardFieldNames.AdditionalContacts => new List<FieldChange> { ApplyAdditionalContacts(workOrder, value, auditField) },
|
||||
WorkOrderBoardFieldNames.FlagColor => new List<FieldChange> { ApplyFlagColor(workOrder, value, auditField) },
|
||||
WorkOrderBoardFieldNames.AvetaRequired => new List<FieldChange> { 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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<WorkOrderBoardValidationException>(() =>
|
||||
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.AvetaRequired,
|
||||
Value = "yes",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1"));
|
||||
|
||||
Assert.Equal("InvalidValue", ex.Code);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<ApplicationDbContext>()
|
||||
.UseSqlite("DataSource=:memory:")
|
||||
.Options;
|
||||
|
||||
using var context = new ApplicationDbContext(options);
|
||||
var migrations = context.Database.GetMigrations().ToList();
|
||||
|
||||
Assert.Contains("20260824150000_SH117_AvetaRequired", migrations);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue