Merge pull request #74 from Sea-Haven-Industries/fix/sh-183-board-inactive-vendor
Some checks are pending
Validate and deploy dev / Validate deployable source bundle (push) Waiting to run
Validate and deploy dev / Deploy shoc-backend to Elastic Beanstalk dev (push) Blocked by required conditions

fix(work-orders): omit inactive primary vendor from board rows (SH-183)
This commit is contained in:
Arthur Bassi 2026-08-20 13:55:00 -03:00 • committed by GitHub
commit 737c137a82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 254 additions and 4 deletions

View file

@ -0,0 +1,28 @@
namespace SeaHaven.DataServices.Helpers
{
/// <summary>
/// SH-183: a cancelled or refused primary dispatch is not a live vendor assignment.
/// Verified/Completed remain live — the company that did the work.
/// </summary>
public static class WorkOrderBoardDispatchAssignment
{
public static bool IsInactivePrimaryDispatch(string? status)
{
var normalized = (status ?? string.Empty).Trim().ToLowerInvariant();
return normalized is "cancelled" or "canceled" or "refused";
}
public static (int? VendorId, string? VendorName) LiveVendor(
int? vendorId,
string? vendorName,
string? primaryDispatchStatus)
{
if (IsInactivePrimaryDispatch(primaryDispatchStatus))
{
return (null, null);
}
return (vendorId, vendorName);
}
}
}

View file

@ -50,6 +50,7 @@ namespace SeaHaven.DataServices.Helpers
w.ScheduledEnd,
w.TargetWeek,
w.ScheduleWeekOnly,
PrimaryDispatchStatus = w.PrimaryDispatch != null ? w.PrimaryDispatch.Status : null,
VendorId = w.PrimaryDispatch != null ? (int?)w.PrimaryDispatch.VendorId : null,
VendorName = w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null
? w.PrimaryDispatch.Vendor.CompanyName
@ -124,6 +125,10 @@ namespace SeaHaven.DataServices.Helpers
var techPhone = !string.IsNullOrWhiteSpace(w.DispatchTechPhone)
? w.DispatchTechPhone
: (!string.IsNullOrWhiteSpace(w.WoTechPhone) ? w.WoTechPhone : w.VendorPhone);
var (vendorId, vendorName) = WorkOrderBoardDispatchAssignment.LiveVendor(
w.VendorId,
w.VendorName,
w.PrimaryDispatchStatus);
return new WorkOrderBoardRawRow(
w.Id,
@ -150,8 +155,8 @@ namespace SeaHaven.DataServices.Helpers
w.ScheduledEnd,
w.TargetWeek,
w.ScheduleWeekOnly,
w.VendorId,
w.VendorName,
vendorId,
vendorName,
w.TechName,
techPhone,
w.DispatchApptDate,
@ -170,6 +175,7 @@ namespace SeaHaven.DataServices.Helpers
w.HasUplift,
w.PrimaryUpliftStatus,
w.PrimaryUpliftAmount,
w.PrimaryDispatchStatus,
isUnscheduled);
}).ToList();
}

View file

@ -58,6 +58,7 @@ namespace SeaHaven.DataServices.Interfaces
bool HasUplift,
string? PrimaryUpliftStatus,
decimal? PrimaryUpliftAmount,
string? PrimaryDispatchStatus,
bool IsUnscheduled);
public record WorkOrderBoardQueryResult(

View file

@ -58,6 +58,10 @@ namespace SeaHaven.Services.DTOs
public string? DayGroup { get; set; }
public int? VendorId { get; set; }
public string? VendorName { get; set; }
/// <summary>
/// Primary dispatch status. Inactive statuses are not a live company assignment.
/// </summary>
public string? PrimaryDispatchStatus { get; set; }
public string? TechName { get; set; }
public string? TechPhone { get; set; }
public DateTime? ApptDate { get; set; }

View file

@ -1,5 +1,6 @@
using System.Security.Claims;
using Data.SeaHavenIndustries.Enums;
using SeaHaven.DataServices.Helpers;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Helpers;
@ -86,6 +87,10 @@ namespace SeaHaven.Services.Implementation
// Time prefers WO ScheduledStart/End (dispatcher window), then dispatch datetime.
var apptStart = row.ScheduledStart ?? row.DispatchApptDate;
var apptEnd = row.ScheduledEnd;
var (vendorId, vendorName) = WorkOrderBoardDispatchAssignment.LiveVendor(
row.VendorId,
row.VendorName,
row.PrimaryDispatchStatus);
return new WorkOrderBoardRowDto
{
@ -113,8 +118,9 @@ namespace SeaHaven.Services.Implementation
TargetWeek = row.TargetWeek,
ScheduleWeekOnly = row.ScheduleWeekOnly,
DayGroup = WorkOrderDerivedFields.GetDayGroup(row.ScheduledDate),
VendorId = row.VendorId,
VendorName = row.VendorName,
VendorId = vendorId,
VendorName = vendorName,
PrimaryDispatchStatus = row.PrimaryDispatchStatus,
TechName = row.TechName,
TechPhone = row.TechPhone,
ApptDate = row.DispatchApptDate ?? row.ScheduledDate,

View file

@ -118,6 +118,7 @@ namespace SeaHaven.Services.Implementation
DayGroup = row.DayGroup,
VendorId = row.VendorId,
VendorName = row.VendorName,
PrimaryDispatchStatus = row.PrimaryDispatchStatus,
TechName = row.TechName,
TechPhone = row.TechPhone,
ApptDate = row.ApptDate,

View file

@ -0,0 +1,35 @@
using SeaHaven.DataServices.Helpers;
namespace SeaHavenIndustries.Tests;
public class WorkOrderBoardDispatchAssignmentTests
{
[Theory]
[InlineData("Cancelled")]
[InlineData("Canceled")]
[InlineData("Refused")]
[InlineData(" cancelled ")]
[InlineData("CANCELED")]
[InlineData("refused")]
public void LiveVendor_InactiveStatus_ClearsAssignment(string status)
{
var (vendorId, vendorName) = WorkOrderBoardDispatchAssignment.LiveVendor(45, "Acme HVAC", status);
Assert.Null(vendorId);
Assert.Null(vendorName);
Assert.True(WorkOrderBoardDispatchAssignment.IsInactivePrimaryDispatch(status));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("Sent")]
[InlineData("Verified")]
[InlineData("Completed")]
public void LiveVendor_LiveOrMissingStatus_KeepsVendorIdWhenNameEmpty(string? status)
{
var (vendorId, vendorName) = WorkOrderBoardDispatchAssignment.LiveVendor(45, null, status);
Assert.Equal(45, vendorId);
Assert.Null(vendorName);
Assert.False(WorkOrderBoardDispatchAssignment.IsInactivePrimaryDispatch(status));
}
}

View file

@ -0,0 +1,102 @@
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Implementation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.Implementation;
namespace SeaHavenIndustries.Tests;
public class WorkOrderBoardInactiveVendorTests
{
private static ApplicationDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ApplicationDbContext(options);
}
private static async Task SeedPrimaryDispatchAsync(
ApplicationDbContext context,
string? dispatchStatus,
string? companyName)
{
context.Vendors.Add(new Vendor { Id = 1, CompanyName = companyName });
context.Dispatches.Add(new Dispatch
{
Id = 10,
VendorId = 1,
WorkOrderId = 1,
Status = dispatchStatus,
ScheduledDate = new DateTime(2026, 6, 24, 8, 0, 0)
});
context.workOrders.Add(new WorkOrder
{
Id = 1,
InternalWONumber = "10000000001",
ScheduledDate = new DateTime(2026, 6, 23),
LifecycleStatus = LifecycleStatus.Scheduled,
PrimaryDispatchId = 10,
SiteCode = "BK5"
});
await context.SaveChangesAsync();
}
[Theory]
[InlineData("Cancelled")]
[InlineData("Canceled")]
[InlineData("Refused")]
public async Task GetBoardRows_InactivePrimaryDispatch_OmitsVendorAssignment(string status)
{
await using var context = CreateContext();
await SeedPrimaryDispatchAsync(context, status, "Acme HVAC");
var weekStart = new DateOnly(2026, 6, 22);
var result = await new WorkOrderBoardDataService(context).GetBoardRowsAsync(new WorkOrderBoardQuery(
weekStart,
weekStart.AddDays(4),
null,
false,
null,
false,
null,
null));
var raw = Assert.Single(result.ScheduledRows);
Assert.Null(raw.VendorId);
Assert.Null(raw.VendorName);
Assert.Equal(status, raw.PrimaryDispatchStatus);
var dto = WorkOrderBoardService.MapRawRow(raw, DateTime.UtcNow);
Assert.Null(dto.VendorId);
Assert.Null(dto.VendorName);
Assert.Equal(status, dto.PrimaryDispatchStatus);
}
[Fact]
public async Task GetBoardRows_LiveDispatchWithEmptyCompanyName_KeepsVendorId()
{
await using var context = CreateContext();
await SeedPrimaryDispatchAsync(context, "Sent", "");
var weekStart = new DateOnly(2026, 6, 22);
var result = await new WorkOrderBoardDataService(context).GetBoardRowsAsync(new WorkOrderBoardQuery(
weekStart,
weekStart.AddDays(4),
null,
false,
null,
false,
null,
null));
var raw = Assert.Single(result.ScheduledRows);
Assert.Equal(1, raw.VendorId);
Assert.Equal("Sent", raw.PrimaryDispatchStatus);
var dto = WorkOrderBoardService.MapRawRow(raw, DateTime.UtcNow);
Assert.Equal(1, dto.VendorId);
Assert.Equal("Sent", dto.PrimaryDispatchStatus);
}
}

View file

@ -216,6 +216,73 @@ public class WorkOrderDetailServiceTests
Assert.Equal(1500m, detail.Info.UpliftSummary.Amount);
}
[Fact]
public async Task GetDetail_InactivePrimaryDispatch_OmitsVendorAndMapsStatus()
{
var (context, service) = CreateSut();
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
context.Dispatches.Add(new Dispatch
{
Id = 10,
VendorId = 1,
WorkOrderId = 1,
Status = "Refused",
ScheduledDate = new DateTime(2026, 6, 24, 8, 0, 0)
});
context.workOrders.Add(new WorkOrder
{
Id = 1,
InternalWONumber = "10000000001",
LifecycleStatus = LifecycleStatus.Scheduled,
ScheduledDate = DateTime.UtcNow.AddDays(1),
PrimaryDispatchId = 10,
SiteCode = "BK5",
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
});
await context.SaveChangesAsync();
var detail = await service.GetDetailAsync(1, WorkOrderAccountTestHelpers.OrgWideAdmin());
Assert.NotNull(detail);
Assert.Null(detail!.Info.VendorId);
Assert.Null(detail.Info.VendorName);
Assert.Equal("Refused", detail.Info.PrimaryDispatchStatus);
}
[Fact]
public async Task GetDetail_LiveDispatchWithEmptyCompanyName_KeepsVendorIdAndMapsStatus()
{
var (context, service) = CreateSut();
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "" });
context.Dispatches.Add(new Dispatch
{
Id = 10,
VendorId = 1,
WorkOrderId = 1,
Status = "Sent",
ScheduledDate = new DateTime(2026, 6, 24, 8, 0, 0)
});
context.workOrders.Add(new WorkOrder
{
Id = 1,
InternalWONumber = "10000000001",
LifecycleStatus = LifecycleStatus.Scheduled,
ScheduledDate = DateTime.UtcNow.AddDays(1),
PrimaryDispatchId = 10,
SiteCode = "BK5",
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
});
await context.SaveChangesAsync();
var detail = await service.GetDetailAsync(1, WorkOrderAccountTestHelpers.OrgWideAdmin());
Assert.NotNull(detail);
Assert.Equal(1, detail!.Info.VendorId);
Assert.Equal("Sent", detail.Info.PrimaryDispatchStatus);
}
[Fact]
public async Task GetDetail_ReturnsAdditionalContactsFromBoardRow()
{