2026-07-23 12:55:47 -03:00
|
|
|
using Data.SeaHavenIndustries;
|
2026-07-23 17:02:40 -03:00
|
|
|
using Data.SeaHavenIndustries.Enums;
|
2026-07-23 12:55:47 -03:00
|
|
|
using FluentAssertions;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using SeaHaven.DataServices.Implementation;
|
|
|
|
|
using Xunit;
|
|
|
|
|
|
|
|
|
|
namespace Api.SeaHavenIndustries.Tests;
|
|
|
|
|
|
|
|
|
|
public class VendorDataServiceTests
|
|
|
|
|
{
|
|
|
|
|
private static ApplicationDbContext NewContext()
|
|
|
|
|
{
|
|
|
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
|
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
|
|
|
.Options;
|
|
|
|
|
return new ApplicationDbContext(options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static ApplicationDbContext NewSqlServerContext()
|
|
|
|
|
{
|
|
|
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
|
|
|
.UseSqlServer("Server=(local);Database=TranslationOnly;Trusted_Connection=True;TrustServerCertificate=True")
|
|
|
|
|
.Options;
|
|
|
|
|
return new ApplicationDbContext(options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Vendor VendorWithJobs(
|
|
|
|
|
string company,
|
|
|
|
|
string city,
|
|
|
|
|
string state,
|
|
|
|
|
string trades,
|
|
|
|
|
int jobCount,
|
|
|
|
|
bool isActive = true)
|
|
|
|
|
{
|
|
|
|
|
return new Vendor
|
|
|
|
|
{
|
|
|
|
|
CompanyName = company,
|
|
|
|
|
ContactName = $"{company} Technician",
|
|
|
|
|
City = city,
|
|
|
|
|
State = state,
|
|
|
|
|
TradeSpecialties = trades,
|
|
|
|
|
IsActive = isActive,
|
|
|
|
|
IsDeleted = false,
|
|
|
|
|
Dispatches = Enumerable.Range(1, jobCount)
|
|
|
|
|
.Select(workOrderId => new Dispatch { WorkOrderId = workOrderId })
|
|
|
|
|
.ToList()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task SeedBoundaryVendors(ApplicationDbContext context)
|
|
|
|
|
{
|
|
|
|
|
context.Vendors.AddRange(
|
|
|
|
|
VendorWithJobs("Under Co", "Chicago", "IL", "Plumbing, HVAC", 49),
|
|
|
|
|
VendorWithJobs("Fifty Co", "Chicago", "IL", "Electrical", 50),
|
|
|
|
|
VendorWithJobs("Hundred Co", "Dallas", "TX", "Plumbing", 100),
|
|
|
|
|
VendorWithJobs("One Fifty Co", "Dallas", "TX", "Roofing", 150));
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData("under-50", "Under Co")]
|
|
|
|
|
[InlineData("50-99", "Fifty Co")]
|
|
|
|
|
[InlineData("100-149", "Hundred Co")]
|
|
|
|
|
[InlineData("150-plus", "One Fifty Co")]
|
|
|
|
|
public async Task GetPagedAsync_AppliesExactJobBucketBoundaries(
|
|
|
|
|
string bucket,
|
|
|
|
|
string expectedCompany)
|
|
|
|
|
{
|
|
|
|
|
await using var context = NewContext();
|
|
|
|
|
await SeedBoundaryVendors(context);
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
|
|
|
|
|
var (items, totalCount) = await service.GetPagedAsync(
|
|
|
|
|
1,
|
|
|
|
|
12,
|
|
|
|
|
isActive: true,
|
|
|
|
|
jobBuckets: new[] { bucket });
|
|
|
|
|
|
|
|
|
|
totalCount.Should().Be(1);
|
|
|
|
|
items.Single().CompanyName.Should().Be(expectedCompany);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetPagedAsync_UsesOrWithinFacetAndAndAcrossFacets()
|
|
|
|
|
{
|
|
|
|
|
await using var context = NewContext();
|
|
|
|
|
await SeedBoundaryVendors(context);
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
|
|
|
|
|
var (items, totalCount) = await service.GetPagedAsync(
|
|
|
|
|
1,
|
|
|
|
|
12,
|
|
|
|
|
isActive: true,
|
|
|
|
|
companies: new[] { "Under Co", "Hundred Co" },
|
|
|
|
|
trades: new[] { "HVAC", "Plumbing" },
|
|
|
|
|
locations: new[] { "Chicago, IL" },
|
|
|
|
|
jobBuckets: new[] { "under-50", "100-149" });
|
|
|
|
|
|
|
|
|
|
totalCount.Should().Be(1);
|
|
|
|
|
items.Single().CompanyName.Should().Be("Under Co");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetPagedAsync_StatusAndCountAreIndependentOfInactiveHistory()
|
|
|
|
|
{
|
|
|
|
|
await using var context = NewContext();
|
|
|
|
|
context.Vendors.AddRange(
|
|
|
|
|
VendorWithJobs("Active Co", "Chicago", "IL", "Plumbing", 1),
|
|
|
|
|
VendorWithJobs("Inactive Co", "Chicago", "IL", "Plumbing", 1, false));
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
|
|
|
|
|
var active = await service.GetPagedAsync(1, 1, isActive: true);
|
|
|
|
|
var inactive = await service.GetPagedAsync(1, 1, isActive: false);
|
|
|
|
|
|
|
|
|
|
active.TotalCount.Should().Be(1);
|
|
|
|
|
inactive.TotalCount.Should().Be(1);
|
|
|
|
|
active.Items.Single().CompanyName.Should().Be("Active Co");
|
|
|
|
|
inactive.Items.Single().CompanyName.Should().Be("Inactive Co");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetPagedAsync_LocationFacetMatchesCityStateAndFullLabels()
|
|
|
|
|
{
|
|
|
|
|
await using var context = NewContext();
|
|
|
|
|
context.Vendors.AddRange(
|
|
|
|
|
new Vendor { CompanyName = "CityOnly", City = "Austin", State = null, TradeSpecialties = "Plumbing", IsActive = true, IsDeleted = false },
|
|
|
|
|
new Vendor { CompanyName = "StateOnly", City = null, State = "TX", TradeSpecialties = "Plumbing", IsActive = true, IsDeleted = false },
|
|
|
|
|
new Vendor { CompanyName = "FullLoc", City = "Austin", State = "TX", TradeSpecialties = "Plumbing", IsActive = true, IsDeleted = false });
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
|
|
|
|
|
var cityOnly = await service.GetPagedAsync(1, 12, isActive: true, locations: new[] { "Austin" });
|
|
|
|
|
cityOnly.TotalCount.Should().Be(1);
|
|
|
|
|
cityOnly.Items.Single().CompanyName.Should().Be("CityOnly");
|
|
|
|
|
|
|
|
|
|
var stateOnly = await service.GetPagedAsync(1, 12, isActive: true, locations: new[] { "TX" });
|
|
|
|
|
stateOnly.TotalCount.Should().Be(1);
|
|
|
|
|
stateOnly.Items.Single().CompanyName.Should().Be("StateOnly");
|
|
|
|
|
|
|
|
|
|
var full = await service.GetPagedAsync(1, 12, isActive: true, locations: new[] { "Austin, TX" });
|
|
|
|
|
full.TotalCount.Should().Be(1);
|
|
|
|
|
full.Items.Single().CompanyName.Should().Be("FullLoc");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetPagedAsync_TradeFacetMatchesExactTokensNotSubstrings()
|
|
|
|
|
{
|
|
|
|
|
await using var context = NewContext();
|
|
|
|
|
context.Vendors.AddRange(
|
|
|
|
|
new Vendor { CompanyName = "HvacOnly", City = "Chicago", State = "IL", TradeSpecialties = "HVAC", IsActive = true, IsDeleted = false },
|
|
|
|
|
new Vendor { CompanyName = "HvacControls", City = "Chicago", State = "IL", TradeSpecialties = "HVAC Controls", IsActive = true, IsDeleted = false },
|
|
|
|
|
new Vendor { CompanyName = "PlumbingCo", City = "Chicago", State = "IL", TradeSpecialties = "Plumbing", IsActive = true, IsDeleted = false });
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
|
|
|
|
|
var hvac = await service.GetPagedAsync(1, 12, isActive: true, trades: new[] { "HVAC" });
|
|
|
|
|
hvac.TotalCount.Should().Be(1);
|
|
|
|
|
hvac.Items.Single().CompanyName.Should().Be("HvacOnly");
|
|
|
|
|
|
|
|
|
|
var hvacControls = await service.GetPagedAsync(1, 12, isActive: true, trades: new[] { "HVAC Controls" });
|
|
|
|
|
hvacControls.TotalCount.Should().Be(1);
|
|
|
|
|
hvacControls.Items.Single().CompanyName.Should().Be("HvacControls");
|
|
|
|
|
|
|
|
|
|
var plumb = await service.GetPagedAsync(1, 12, isActive: true, trades: new[] { "Plumb" });
|
|
|
|
|
plumb.TotalCount.Should().Be(0);
|
|
|
|
|
plumb.Items.Should().BeEmpty();
|
|
|
|
|
|
|
|
|
|
var plumbing = await service.GetPagedAsync(1, 12, isActive: true, trades: new[] { "Plumbing" });
|
|
|
|
|
plumbing.TotalCount.Should().Be(1);
|
|
|
|
|
plumbing.Items.Single().CompanyName.Should().Be("PlumbingCo");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetPagedAsync_TradeFacetMatchesTokenInCommaDelimitedList()
|
|
|
|
|
{
|
|
|
|
|
await using var context = NewContext();
|
|
|
|
|
context.Vendors.AddRange(
|
|
|
|
|
new Vendor { CompanyName = "Multi", City = "Chicago", State = "IL", TradeSpecialties = "Plumbing, HVAC", IsActive = true, IsDeleted = false },
|
|
|
|
|
new Vendor { CompanyName = "Middle", City = "Chicago", State = "IL", TradeSpecialties = "Electrical, Plumbing, Roofing", IsActive = true, IsDeleted = false });
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
|
|
|
|
|
var plumbing = await service.GetPagedAsync(1, 12, isActive: true, trades: new[] { "Plumbing" });
|
|
|
|
|
plumbing.TotalCount.Should().Be(2);
|
|
|
|
|
plumbing.Items.Select(v => v.CompanyName).Should().BeEquivalentTo("Multi", "Middle");
|
|
|
|
|
|
|
|
|
|
var roofing = await service.GetPagedAsync(1, 12, isActive: true, trades: new[] { "Roofing" });
|
|
|
|
|
roofing.TotalCount.Should().Be(1);
|
|
|
|
|
roofing.Items.Single().CompanyName.Should().Be("Middle");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetPagedAsync_CountsDistinctDirectAndLinkedWorkOrders()
|
|
|
|
|
{
|
|
|
|
|
await using var context = NewContext();
|
|
|
|
|
var vendor = new Vendor
|
|
|
|
|
{
|
|
|
|
|
CompanyName = "History Co",
|
|
|
|
|
IsActive = true,
|
|
|
|
|
IsDeleted = false,
|
|
|
|
|
Dispatches = new List<Dispatch>
|
|
|
|
|
{
|
|
|
|
|
new()
|
|
|
|
|
{
|
|
|
|
|
WorkOrderId = 100,
|
|
|
|
|
DispatchWorkOrders = new List<DispatchWorkOrder>
|
|
|
|
|
{
|
|
|
|
|
new() { WorkOrderId = 100 },
|
|
|
|
|
new() { WorkOrderId = 101 }
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
new() { WorkOrderId = 101 }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
context.Vendors.Add(vendor);
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
var result = await service.GetPagedAsync(1, 12, isActive: true);
|
|
|
|
|
|
|
|
|
|
result.Items.Single().TotalJobs.Should().Be(2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetByIdAsync_ReturnsTrackedVendorWithoutLoadingDispatchGraph()
|
|
|
|
|
{
|
|
|
|
|
await using var context = NewContext();
|
|
|
|
|
var vendor = VendorWithJobs("Lean Co", "Chicago", "IL", "Plumbing", 2);
|
|
|
|
|
context.Vendors.Add(vendor);
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
context.ChangeTracker.Clear();
|
|
|
|
|
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
var result = await service.GetByIdAsync(vendor.Id);
|
|
|
|
|
|
|
|
|
|
result.Should().NotBeNull();
|
|
|
|
|
context.Entry(result!).State.Should().Be(EntityState.Unchanged);
|
|
|
|
|
context.Entry(result!).Collection(nameof(Vendor.Dispatches)).IsLoaded.Should().BeFalse();
|
|
|
|
|
result!.Dispatches.Should().BeNull();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData("trade")]
|
|
|
|
|
[InlineData("location")]
|
|
|
|
|
[InlineData("job-bucket")]
|
|
|
|
|
public void BuildDirectoryQuery_TranslatesRepresentativeFiltersForSqlServer(string filter)
|
|
|
|
|
{
|
|
|
|
|
using var context = NewSqlServerContext();
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
|
|
|
|
|
var query = filter switch
|
|
|
|
|
{
|
|
|
|
|
"trade" => service.BuildDirectoryQuery(trades: new[] { "Plumbing" }),
|
|
|
|
|
"location" => service.BuildDirectoryQuery(locations: new[] { "Chicago, IL" }),
|
|
|
|
|
_ => service.BuildDirectoryQuery(jobBuckets: new[] { "50-99" })
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var sql = query.ToQueryString();
|
|
|
|
|
|
|
|
|
|
sql.Should().Contain("SELECT");
|
|
|
|
|
if (filter == "trade")
|
|
|
|
|
sql.Should().Contain("TradeSpecialties");
|
|
|
|
|
if (filter == "location")
|
|
|
|
|
sql.Should().Contain("City");
|
|
|
|
|
if (filter == "job-bucket")
|
|
|
|
|
sql.Should().Contain("UNION");
|
|
|
|
|
}
|
2026-07-23 17:02:40 -03:00
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void BuildOpenLinkedWorkOrdersQuery_TranslatesForSqlServerWithoutClientEvaluation()
|
|
|
|
|
{
|
|
|
|
|
using var context = NewSqlServerContext();
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
|
|
|
|
|
var sql = service.BuildOpenLinkedWorkOrdersQuery(60).ToQueryString();
|
|
|
|
|
|
|
|
|
|
sql.Should().Contain("SELECT");
|
|
|
|
|
sql.Should().Contain("Dispatches");
|
|
|
|
|
sql.Should().Contain("DispatchWorkOrders");
|
|
|
|
|
sql.Should().Contain("workOrders");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetOpenLinkedWorkOrdersAsync_DedupsToOneRecordPerWorkOrderAcrossDirectAndMultiLinks()
|
|
|
|
|
{
|
|
|
|
|
await using var context = NewContext();
|
|
|
|
|
var vendor = new Vendor { Id = 60, CompanyName = "Linked Co", IsActive = true };
|
|
|
|
|
context.Vendors.Add(vendor);
|
|
|
|
|
context.workOrders.AddRange(
|
|
|
|
|
new WorkOrder
|
|
|
|
|
{
|
|
|
|
|
Id = 701,
|
|
|
|
|
WorkerOrderNumber = "WO-701",
|
|
|
|
|
WorkerOrderTitle = "Direct And Multi",
|
|
|
|
|
Status = "Scheduled",
|
|
|
|
|
LifecycleStatus = LifecycleStatus.Scheduled
|
|
|
|
|
},
|
|
|
|
|
new WorkOrder
|
|
|
|
|
{
|
|
|
|
|
Id = 702,
|
|
|
|
|
WorkerOrderNumber = "WO-702",
|
|
|
|
|
WorkerOrderTitle = "Multi Only",
|
|
|
|
|
Status = "Scheduled",
|
|
|
|
|
LifecycleStatus = LifecycleStatus.Scheduled
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var directDispatch = new Dispatch { Id = 7001, VendorId = 60, WorkOrderId = 701, Vendor = vendor };
|
|
|
|
|
var multiDispatch = new Dispatch { Id = 7002, VendorId = 60, WorkOrderId = null, Vendor = vendor };
|
|
|
|
|
context.Dispatches.AddRange(directDispatch, multiDispatch);
|
|
|
|
|
context.DispatchWorkOrders.AddRange(
|
|
|
|
|
new DispatchWorkOrder { DispatchId = 7002, Dispatch = multiDispatch, WorkOrderId = 702 },
|
|
|
|
|
new DispatchWorkOrder { DispatchId = 7002, Dispatch = multiDispatch, WorkOrderId = 701 });
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
var service = new VendorDataService(context);
|
|
|
|
|
var result = await service.GetOpenLinkedWorkOrdersAsync(60);
|
|
|
|
|
|
|
|
|
|
result.Should().HaveCount(2);
|
|
|
|
|
result.Select(x => x.WorkOrderId).Should().BeEquivalentTo(new[] { 701, 702 });
|
|
|
|
|
result.Select(x => x.WorkOrderId).Should().OnlyHaveUniqueItems();
|
|
|
|
|
}
|
2026-07-23 12:55:47 -03:00
|
|
|
}
|