mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
fix(vendors): align facet filtering
This commit is contained in:
parent
9a35db58b3
commit
d649940a8f
2 changed files with 144 additions and 7 deletions
|
|
@ -110,4 +110,75 @@ public class VendorDataServiceTests
|
||||||
active.Items.Single().CompanyName.Should().Be("Active Co");
|
active.Items.Single().CompanyName.Should().Be("Active Co");
|
||||||
inactive.Items.Single().CompanyName.Should().Be("Inactive 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Implementation
|
namespace SeaHaven.DataServices.Implementation
|
||||||
|
|
@ -87,7 +88,7 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToArray() ?? Array.Empty<string>();
|
.ToArray() ?? Array.Empty<string>();
|
||||||
if (selectedLocations.Length > 0)
|
if (selectedLocations.Length > 0)
|
||||||
query = query.Where(v => selectedLocations.Contains(((v.City ?? "") + ", " + (v.State ?? "")).ToLower()));
|
query = query.Where(BuildLocationPredicate(selectedLocations));
|
||||||
|
|
||||||
var selectedJobBuckets = jobBuckets?
|
var selectedJobBuckets = jobBuckets?
|
||||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||||
|
|
@ -150,6 +151,9 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
.Count();
|
.Count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly MethodInfo StringConcatMethod =
|
||||||
|
typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) })!;
|
||||||
|
|
||||||
private static Expression<Func<Vendor, bool>> BuildTradePredicate(
|
private static Expression<Func<Vendor, bool>> BuildTradePredicate(
|
||||||
IReadOnlyCollection<string> selectedTrades)
|
IReadOnlyCollection<string> selectedTrades)
|
||||||
{
|
{
|
||||||
|
|
@ -158,21 +162,83 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
Expression.Property(vendor, nameof(Vendor.TradeSpecialties)),
|
Expression.Property(vendor, nameof(Vendor.TradeSpecialties)),
|
||||||
Expression.Constant(string.Empty));
|
Expression.Constant(string.Empty));
|
||||||
var lowered = Expression.Call(specialties, nameof(string.ToLower), Type.EmptyTypes);
|
var lowered = Expression.Call(specialties, nameof(string.ToLower), Type.EmptyTypes);
|
||||||
|
|
||||||
|
var delimiters = new[] { ", ", "," };
|
||||||
Expression? body = null;
|
Expression? body = null;
|
||||||
|
|
||||||
foreach (var trade in selectedTrades)
|
foreach (var trade in selectedTrades)
|
||||||
{
|
{
|
||||||
var contains = Expression.Call(
|
var token = Expression.Constant(trade);
|
||||||
lowered,
|
|
||||||
nameof(string.Contains),
|
body = OrElse(body, Expression.Equal(lowered, token));
|
||||||
Type.EmptyTypes,
|
|
||||||
Expression.Constant(trade));
|
foreach (var delimiter in delimiters)
|
||||||
body = body == null ? contains : Expression.OrElse(body, contains);
|
{
|
||||||
|
var delim = Expression.Constant(delimiter);
|
||||||
|
var tokenWithTrailing = Concat(token, delim);
|
||||||
|
var leadingToken = Concat(delim, token);
|
||||||
|
var wrappedToken = Concat(leadingToken, delim);
|
||||||
|
|
||||||
|
body = OrElse(body, StartsWith(lowered, tokenWithTrailing));
|
||||||
|
body = OrElse(body, EndsWith(lowered, leadingToken));
|
||||||
|
body = OrElse(body, Contains(lowered, wrappedToken));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Expression.Lambda<Func<Vendor, bool>>(body ?? Expression.Constant(false), vendor);
|
return Expression.Lambda<Func<Vendor, bool>>(body ?? Expression.Constant(false), vendor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Expression<Func<Vendor, bool>> BuildLocationPredicate(
|
||||||
|
IReadOnlyCollection<string> selectedLocations)
|
||||||
|
{
|
||||||
|
var vendor = Expression.Parameter(typeof(Vendor), "vendor");
|
||||||
|
var label = BuildLocationLabelBody(vendor);
|
||||||
|
var lowered = Expression.Call(label, nameof(string.ToLower), Type.EmptyTypes);
|
||||||
|
|
||||||
|
Expression? body = null;
|
||||||
|
foreach (var location in selectedLocations)
|
||||||
|
body = OrElse(body, Expression.Equal(lowered, Expression.Constant(location)));
|
||||||
|
|
||||||
|
return Expression.Lambda<Func<Vendor, bool>>(body ?? Expression.Constant(false), vendor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression BuildLocationLabelBody(ParameterExpression vendor)
|
||||||
|
{
|
||||||
|
var city = Expression.Property(vendor, nameof(Vendor.City));
|
||||||
|
var state = Expression.Property(vendor, nameof(Vendor.State));
|
||||||
|
var cityBlank = IsBlank(city);
|
||||||
|
var stateBlank = IsBlank(state);
|
||||||
|
|
||||||
|
var cityCommaState = Concat(Concat(city, Expression.Constant(", ")), state);
|
||||||
|
|
||||||
|
var onlyState = Expression.Condition(stateBlank, Expression.Constant(string.Empty), state);
|
||||||
|
var cityOrBoth = Expression.Condition(stateBlank, city, cityCommaState);
|
||||||
|
return Expression.Condition(cityBlank, onlyState, cityOrBoth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression IsBlank(Expression value)
|
||||||
|
{
|
||||||
|
var isNull = Expression.Equal(value, Expression.Constant(null, value.Type));
|
||||||
|
var trimmed = Expression.Call(value, nameof(string.Trim), Type.EmptyTypes);
|
||||||
|
var isEmpty = Expression.Equal(trimmed, Expression.Constant(string.Empty));
|
||||||
|
return Expression.Condition(isNull, Expression.Constant(true), isEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression Concat(Expression left, Expression right) =>
|
||||||
|
Expression.Call(StringConcatMethod, left, right);
|
||||||
|
|
||||||
|
private static Expression StartsWith(Expression instance, Expression value) =>
|
||||||
|
Expression.Call(instance, nameof(string.StartsWith), Type.EmptyTypes, value);
|
||||||
|
|
||||||
|
private static Expression EndsWith(Expression instance, Expression value) =>
|
||||||
|
Expression.Call(instance, nameof(string.EndsWith), Type.EmptyTypes, value);
|
||||||
|
|
||||||
|
private static Expression Contains(Expression instance, Expression value) =>
|
||||||
|
Expression.Call(instance, nameof(string.Contains), Type.EmptyTypes, value);
|
||||||
|
|
||||||
|
private static Expression OrElse(Expression? accumulated, Expression next) =>
|
||||||
|
accumulated is null ? next : Expression.OrElse(accumulated, next);
|
||||||
|
|
||||||
public async Task<Vendor> AddAsync(Vendor vendor)
|
public async Task<Vendor> AddAsync(Vendor vendor)
|
||||||
{
|
{
|
||||||
await _context.Vendors.AddAsync(vendor);
|
await _context.Vendors.AddAsync(vendor);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue