mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
fix(vendors): source facets from companies
This commit is contained in:
parent
665880ee80
commit
fc2870869f
5 changed files with 267 additions and 22 deletions
|
|
@ -391,4 +391,107 @@ public class VendorDataServiceTests
|
|||
if (filter == "job-bucket")
|
||||
sql.Should().Contain("TotalJobs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCompaniesForFacetsAsync_IncludesZeroTechnicianCompanyForActiveAndNullStatus()
|
||||
{
|
||||
await using var context = NewContext();
|
||||
var rosterService = new VendorCompanyRosterDataService(context);
|
||||
await rosterService.CreateRosterAsync(new VendorCompanyRosterWriteModel
|
||||
{
|
||||
Name = "Solo Co",
|
||||
Email = "solo@example.com",
|
||||
ActorUserId = "42",
|
||||
Technicians = new List<RosterTechnicianWriteModel>()
|
||||
}, CancellationToken.None);
|
||||
|
||||
var techCompany = new VendorCompany { Name = "Tech Co", NormalizedName = "tech co" };
|
||||
context.VendorCompanies.Add(techCompany);
|
||||
context.Vendors.Add(new Vendor
|
||||
{
|
||||
CompanyName = "Tech Co",
|
||||
ContactName = "Pat",
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
Company = techCompany
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var service = new VendorDataService(context);
|
||||
|
||||
var active = await service.GetCompaniesForFacetsAsync(true, CancellationToken.None);
|
||||
active.Select(c => c.Name).Should().BeEquivalentTo(new[] { "Solo Co", "Tech Co" });
|
||||
|
||||
var noStatus = await service.GetCompaniesForFacetsAsync(null, CancellationToken.None);
|
||||
noStatus.Select(c => c.Name).Should().BeEquivalentTo(new[] { "Solo Co", "Tech Co" });
|
||||
|
||||
var inactive = await service.GetCompaniesForFacetsAsync(false, CancellationToken.None);
|
||||
inactive.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCompaniesForFacetsAsync_InactiveStatusReturnsOnlyCompaniesWithInactiveTechnicians()
|
||||
{
|
||||
await using var context = NewContext();
|
||||
var activeOnlyCo = new VendorCompany { Name = "Active Only Co", NormalizedName = "active only co" };
|
||||
var inactiveOnlyCo = new VendorCompany { Name = "Inactive Only Co", NormalizedName = "inactive only co" };
|
||||
var mixedCo = new VendorCompany { Name = "Mixed Co", NormalizedName = "mixed co" };
|
||||
context.VendorCompanies.AddRange(activeOnlyCo, inactiveOnlyCo, mixedCo);
|
||||
context.Vendors.AddRange(
|
||||
new Vendor { CompanyName = "Active Only Co", IsActive = true, IsDeleted = false, Company = activeOnlyCo },
|
||||
new Vendor { CompanyName = "Inactive Only Co", IsActive = false, IsDeleted = false, Company = inactiveOnlyCo },
|
||||
new Vendor { CompanyName = "Mixed Co", IsActive = true, IsDeleted = false, Company = mixedCo },
|
||||
new Vendor { CompanyName = "Mixed Co", IsActive = false, IsDeleted = false, Company = mixedCo });
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var service = new VendorDataService(context);
|
||||
|
||||
var inactive = await service.GetCompaniesForFacetsAsync(false, CancellationToken.None);
|
||||
inactive.Select(c => c.Name).Should().BeEquivalentTo(new[] { "Inactive Only Co", "Mixed Co" });
|
||||
|
||||
var active = await service.GetCompaniesForFacetsAsync(true, CancellationToken.None);
|
||||
active.Select(c => c.Name).Should().BeEquivalentTo(new[] { "Active Only Co", "Mixed Co" });
|
||||
|
||||
var noStatus = await service.GetCompaniesForFacetsAsync(null, CancellationToken.None);
|
||||
noStatus.Select(c => c.Name).Should().BeEquivalentTo(new[] { "Active Only Co", "Inactive Only Co", "Mixed Co" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCompaniesForFacetsAsync_TreatsCompanyWithOnlyDeletedTechniciansAsZeroTechnician()
|
||||
{
|
||||
await using var context = NewContext();
|
||||
var company = new VendorCompany { Name = "Ghost Co", NormalizedName = "ghost co" };
|
||||
context.VendorCompanies.Add(company);
|
||||
context.Vendors.Add(new Vendor
|
||||
{
|
||||
CompanyName = "Ghost Co",
|
||||
IsActive = true,
|
||||
IsDeleted = true,
|
||||
Company = company
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var service = new VendorDataService(context);
|
||||
|
||||
var active = await service.GetCompaniesForFacetsAsync(true, CancellationToken.None);
|
||||
active.Single().Name.Should().Be("Ghost Co");
|
||||
|
||||
var inactive = await service.GetCompaniesForFacetsAsync(false, CancellationToken.None);
|
||||
inactive.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
[InlineData(null)]
|
||||
public void BuildCompaniesForFacetsQuery_TranslatesToSqlServerWithoutClientEvaluation(bool? isActive)
|
||||
{
|
||||
using var context = NewSqlServerContext();
|
||||
var service = new VendorDataService(context);
|
||||
|
||||
var sql = service.BuildCompaniesForFacetsQuery(isActive).ToQueryString();
|
||||
|
||||
sql.Should().Contain("SELECT");
|
||||
sql.Should().Contain("VendorCompanies");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -552,4 +552,102 @@ public class VendorServiceTests
|
|||
.WithMessage("*404*not found");
|
||||
data.Verify(x => x.GetLinkedWorkOrdersAsync(It.IsAny<int>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFacetsAsync_CompaniesAndLocationsUseCompanyOwnedFieldsOverTechnicianFields()
|
||||
{
|
||||
var data = new Mock<IVendorDataService>();
|
||||
data.Setup(x => x.GetCompaniesForFacetsAsync(true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<VendorCompany>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Owner Co",
|
||||
CompanyPhone = "(555) 000-0001",
|
||||
Email = "owner@example.com",
|
||||
Address = "Owner Addr",
|
||||
City = "OwnerCity",
|
||||
State = "OS",
|
||||
Zip = "00001",
|
||||
GoogleMapsUrl = "https://owner.example/map"
|
||||
}
|
||||
});
|
||||
data.Setup(x => x.GetVendorsForFacetsAsync(true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Vendor>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 10,
|
||||
CompanyName = "Owner Co",
|
||||
ContactName = "Pat",
|
||||
City = "TechCity",
|
||||
State = "TS",
|
||||
Email = "tech@example.com",
|
||||
CompanyPhone = "(555) 000-9999",
|
||||
TradeSpecialties = "Plumbing"
|
||||
}
|
||||
});
|
||||
|
||||
var facets = await NewService(data).GetFacetsAsync(true, CancellationToken.None);
|
||||
|
||||
var company = facets.Companies.Single();
|
||||
company.Name.Should().Be("Owner Co");
|
||||
company.CompanyId.Should().Be(1);
|
||||
company.CompanyPhone.Should().Be("(555) 000-0001");
|
||||
company.Email.Should().Be("owner@example.com");
|
||||
company.Address.Should().Be("Owner Addr");
|
||||
company.City.Should().Be("OwnerCity");
|
||||
company.State.Should().Be("OS");
|
||||
company.Zip.Should().Be("00001");
|
||||
company.GoogleMapsUrl.Should().Be("https://owner.example/map");
|
||||
company.PreferredContact.Should().BeNull();
|
||||
|
||||
var location = facets.Locations.Single();
|
||||
location.City.Should().Be("OwnerCity");
|
||||
location.State.Should().Be("OS");
|
||||
location.Label.Should().Be("OwnerCity, OS");
|
||||
|
||||
facets.Trades.Should().BeEquivalentTo(new[] { "Plumbing" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFacetsAsync_CompaniesAreUniqueAndOrderedCaseInsensitively()
|
||||
{
|
||||
var data = new Mock<IVendorDataService>();
|
||||
data.Setup(x => x.GetCompaniesForFacetsAsync(null, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<VendorCompany>
|
||||
{
|
||||
new() { Id = 1, Name = "Beta Co" },
|
||||
new() { Id = 2, Name = "alpha co" },
|
||||
new() { Id = 3, Name = "Alpha Co" }
|
||||
});
|
||||
data.Setup(x => x.GetVendorsForFacetsAsync(null, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Vendor>());
|
||||
|
||||
var facets = await NewService(data).GetFacetsAsync(null, CancellationToken.None);
|
||||
|
||||
// Case-insensitive dedup collapses "alpha co"/"Alpha Co" to one entry, and
|
||||
// ordering places "alpha co" before "Beta Co" (case-insensitive, not Ordinal).
|
||||
facets.Companies.Select(c => c.Name).Should().BeEquivalentTo(
|
||||
new[] { "alpha co", "Beta Co" },
|
||||
opts => opts.WithStrictOrdering());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFacetsAsync_TradesRemainDerivedFromTechniciansAndIgnoreCompanies()
|
||||
{
|
||||
var data = new Mock<IVendorDataService>();
|
||||
data.Setup(x => x.GetCompaniesForFacetsAsync(true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<VendorCompany> { new() { Id = 1, Name = "No Trade Co" } });
|
||||
data.Setup(x => x.GetVendorsForFacetsAsync(true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Vendor>
|
||||
{
|
||||
new() { Id = 20, CompanyName = "Trade Co", TradeSpecialties = "HVAC, Roofing" }
|
||||
});
|
||||
|
||||
var facets = await NewService(data).GetFacetsAsync(true, CancellationToken.None);
|
||||
|
||||
facets.Trades.Should().BeEquivalentTo(new[] { "HVAC", "Roofing" }, opts => opts.WithStrictOrdering());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,39 @@ namespace SeaHaven.DataServices.Implementation
|
|||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
internal IQueryable<VendorCompany> BuildCompaniesForFacetsQuery(bool? isActive)
|
||||
{
|
||||
var query = _context.VendorCompanies
|
||||
.AsNoTracking();
|
||||
|
||||
if (isActive == true)
|
||||
{
|
||||
// Active: companies with zero non-deleted technicians (company-only rows)
|
||||
// or at least one active non-deleted technician.
|
||||
query = query.Where(c =>
|
||||
!_context.Vendors.Any(v =>
|
||||
v.CompanyId == c.Id && (v.IsDeleted == null || v.IsDeleted == false))
|
||||
|| _context.Vendors.Any(v =>
|
||||
v.CompanyId == c.Id
|
||||
&& (v.IsDeleted == null || v.IsDeleted == false)
|
||||
&& v.IsActive));
|
||||
}
|
||||
else if (isActive == false)
|
||||
{
|
||||
// Inactive: companies with at least one inactive non-deleted technician.
|
||||
query = query.Where(c =>
|
||||
_context.Vendors.Any(v =>
|
||||
v.CompanyId == c.Id
|
||||
&& (v.IsDeleted == null || v.IsDeleted == false)
|
||||
&& !v.IsActive));
|
||||
}
|
||||
|
||||
return query.OrderBy(c => c.Id);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<VendorCompany>> GetCompaniesForFacetsAsync(bool? isActive, CancellationToken cancellationToken)
|
||||
=> await BuildCompaniesForFacetsQuery(isActive).ToListAsync(cancellationToken);
|
||||
|
||||
public async Task<(IEnumerable<Vendor> Items, int TotalCount)> GetPagedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
Task<IEnumerable<Vendor>> GetAllAsync();
|
||||
Task<IReadOnlyList<Vendor>> GetActiveVendorsAsync(CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<Vendor>> GetVendorsForFacetsAsync(bool? isActive, CancellationToken cancellationToken);
|
||||
|
||||
// SH-198 facet fix: company-owned facet profiles sourced from VendorCompanies,
|
||||
// with status semantics aligned to BuildDirectoryUnionQuery. Null returns all
|
||||
// companies; active includes companies with zero non-deleted technicians or at
|
||||
// least one active non-deleted technician; inactive includes companies with at
|
||||
// least one inactive non-deleted technician.
|
||||
Task<IReadOnlyList<VendorCompany>> GetCompaniesForFacetsAsync(bool? isActive, CancellationToken cancellationToken);
|
||||
Task<(IEnumerable<Vendor> Items, int TotalCount)> GetPagedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
|
|
|
|||
|
|
@ -622,25 +622,29 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public async Task<VendorFacetsDTO> GetFacetsAsync(bool? isActive, CancellationToken cancellationToken)
|
||||
{
|
||||
// Companies and Locations are sourced from company-owned VendorCompany fields
|
||||
// (status semantics aligned to the directory union); Trades remain derived from
|
||||
// non-deleted technician Vendors.
|
||||
var companies = await _vendorDataService.GetCompaniesForFacetsAsync(isActive, cancellationToken);
|
||||
var vendors = await _vendorDataService.GetVendorsForFacetsAsync(isActive, cancellationToken);
|
||||
|
||||
var companies = vendors
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v.CompanyName))
|
||||
.GroupBy(v => v.CompanyName!.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||
var companyFacets = companies
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Name))
|
||||
.GroupBy(c => c.Name!.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(v => v.CompanyName)
|
||||
.Select(v => new VendorFacetCompanyDTO
|
||||
.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(c => new VendorFacetCompanyDTO
|
||||
{
|
||||
Name = v.CompanyName,
|
||||
CompanyId = v.CompanyId,
|
||||
CompanyPhone = v.CompanyPhone,
|
||||
Email = v.Email,
|
||||
PreferredContact = v.PreferredContact,
|
||||
Address = v.Address,
|
||||
City = v.City,
|
||||
State = v.State,
|
||||
Zip = v.Zip,
|
||||
GoogleMapsUrl = v.GoogleMapsUrl
|
||||
Name = c.Name,
|
||||
CompanyId = c.Id,
|
||||
CompanyPhone = c.CompanyPhone,
|
||||
Email = c.Email,
|
||||
PreferredContact = null,
|
||||
Address = c.Address,
|
||||
City = c.City,
|
||||
State = c.State,
|
||||
Zip = c.Zip,
|
||||
GoogleMapsUrl = c.GoogleMapsUrl
|
||||
});
|
||||
|
||||
var trades = vendors
|
||||
|
|
@ -649,20 +653,20 @@ namespace SeaHaven.Services.Implementation
|
|||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(value => value);
|
||||
|
||||
var locations = vendors
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v.City) || !string.IsNullOrWhiteSpace(v.State))
|
||||
.Select(v => new VendorFacetLocationDTO
|
||||
var locations = companies
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.City) || !string.IsNullOrWhiteSpace(c.State))
|
||||
.Select(c => new VendorFacetLocationDTO
|
||||
{
|
||||
City = v.City,
|
||||
State = v.State,
|
||||
Label = string.Join(", ", new[] { v.City, v.State }.Where(value => !string.IsNullOrWhiteSpace(value)))
|
||||
City = c.City,
|
||||
State = c.State,
|
||||
Label = string.Join(", ", new[] { c.City, c.State }.Where(value => !string.IsNullOrWhiteSpace(value)))
|
||||
})
|
||||
.DistinctBy(location => location.Label, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(location => location.Label);
|
||||
|
||||
return new VendorFacetsDTO
|
||||
{
|
||||
Companies = companies,
|
||||
Companies = companyFacets,
|
||||
Trades = trades,
|
||||
Locations = locations,
|
||||
JobBuckets = new[]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue