feat(work-orders): persist Aveta Extra Docs media category

Round-trip category 5 on media POST/PATCH/GET and project hasAvetaDocument so pending vs attached survives reopen.
This commit is contained in:
Arthur Bassi 2026-08-25 15:10:45 -03:00
parent 7245897d56
commit 7c7c6bc525
11 changed files with 232 additions and 2 deletions

View file

@ -165,7 +165,8 @@ namespace Data.SeaHavenIndustries.Enums
Before = 1,
After = 2,
Extra = 3,
Completion = 4
Completion = 4,
Aveta = 5
}
}

View file

@ -1,4 +1,5 @@
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
@ -20,6 +21,9 @@ namespace SeaHaven.DataServices.Helpers
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,
@ -180,6 +184,7 @@ namespace SeaHaven.DataServices.Helpers
w.PrimaryUpliftAmount,
w.PrimaryDispatchStatus,
w.AvetaRequired,
w.HasAvetaDocument,
w.OriginalDate,
w.OriginalWeek,
isUnscheduled);

View file

@ -60,6 +60,7 @@ namespace SeaHaven.DataServices.Interfaces
decimal? PrimaryUpliftAmount,
string? PrimaryDispatchStatus,
bool AvetaRequired,
bool HasAvetaDocument,
DateOnly? OriginalDate,
DateOnly? OriginalWeek,
bool IsUnscheduled);

View file

@ -37,6 +37,7 @@ namespace SeaHaven.Services.DTOs
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; }

View file

@ -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)

View file

@ -100,6 +100,7 @@ namespace SeaHaven.Services.Implementation
CarriedOver = row.CarriedOver,
IsAddOn = row.IsAddOn,
AvetaRequired = row.AvetaRequired,
HasAvetaDocument = row.HasAvetaDocument,
OriginalDate = row.OriginalDate,
OriginalWeek = row.OriginalWeek,
WorkOrderType = row.WorkOrderType,

View file

@ -97,6 +97,7 @@ namespace SeaHaven.Services.Implementation
CarriedOver = row.CarriedOver,
IsAddOn = row.IsAddOn,
AvetaRequired = row.AvetaRequired,
HasAvetaDocument = row.HasAvetaDocument,
WorkOrderType = row.WorkOrderType,
IsPastDue = row.IsPastDue,
SiteCode = row.SiteCode,

View file

@ -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]

View file

@ -480,6 +480,77 @@ public class WorkOrderBoardServiceTests
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]

View file

@ -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()
{

View file

@ -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()
{