mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +00:00
feat(locations): filter sites by state
This commit is contained in:
parent
3a4af64a64
commit
436e615510
10 changed files with 362 additions and 37 deletions
|
|
@ -4,6 +4,7 @@ using Api.SeaHavenIndustries.DTOs;
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentAssertions;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
|
@ -25,7 +26,7 @@ public class LocationControllerTests
|
|||
public async Task GetLocationList_ReturnsPaginationEnvelopeWithServiceData()
|
||||
{
|
||||
var service = new Mock<ILocationService>();
|
||||
service.Setup(s => s.GetLocationListPagedAsync(1, 10, "a", It.IsAny<CancellationToken>()))
|
||||
service.Setup(s => s.GetLocationListPagedAsync(1, 10, "a", null, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new PagedResult<LocationDTO>
|
||||
{
|
||||
Items = new List<LocationDTO> { new() { Id = 1, Name = "Site" } },
|
||||
|
|
@ -34,7 +35,7 @@ public class LocationControllerTests
|
|||
PageSize = 10
|
||||
});
|
||||
|
||||
var result = await NewController(service).GetLocationList("a", 1, 10, CancellationToken.None);
|
||||
var result = await NewController(service).GetLocationList("a", 1, 10, cancellationToken: CancellationToken.None);
|
||||
|
||||
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||
var envelope = ok.Value.Should().BeOfType<Pagination_DTO>().Subject;
|
||||
|
|
@ -44,6 +45,43 @@ public class LocationControllerTests
|
|||
envelope.TotalPages.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLocationList_PassesStatesFilterToService()
|
||||
{
|
||||
var service = new Mock<ILocationService>();
|
||||
service.Setup(s => s.GetLocationListPagedAsync(1, 10, null, "tx, mo", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new PagedResult<LocationDTO>
|
||||
{
|
||||
Items = new List<LocationDTO>(),
|
||||
TotalCount = 0,
|
||||
Page = 1,
|
||||
PageSize = 10
|
||||
});
|
||||
|
||||
var result = await NewController(service).GetLocationList(null, 1, 10, "tx, mo", CancellationToken.None);
|
||||
|
||||
result.Should().BeOfType<OkObjectResult>();
|
||||
service.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLocationList_WhenStatesInvalid_ReturnsExistingValidationErrorShape()
|
||||
{
|
||||
var service = new Mock<ILocationService>();
|
||||
service.Setup(s => s.GetLocationListPagedAsync(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new ValidationException(new[]
|
||||
{
|
||||
new ValidationFailure("states", "states must be valid US postal abbreviations: ZZ.")
|
||||
}));
|
||||
|
||||
var result = await NewController(service).GetLocationList(null, 1, 10, "ZZ", CancellationToken.None);
|
||||
|
||||
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
||||
var response = bad.Value.Should().BeOfType<Response>().Subject;
|
||||
response.Status.Should().Be("Validation Error");
|
||||
response.Message.Should().Contain("ZZ");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLocationById_WhenMissing_ReturnsNotFoundAndDoesNotLeakEntity()
|
||||
{
|
||||
|
|
|
|||
89
Api.SeaHavenIndustries.Tests/LocationDataServiceTests.cs
Normal file
89
Api.SeaHavenIndustries.Tests/LocationDataServiceTests.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Implementation;
|
||||
using Xunit;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Tests;
|
||||
|
||||
public class LocationDataServiceTests
|
||||
{
|
||||
private static ApplicationDbContext NewContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new ApplicationDbContext(options);
|
||||
}
|
||||
|
||||
private static async Task SeedAsync(ApplicationDbContext ctx)
|
||||
{
|
||||
ctx.Locations.AddRange(
|
||||
new Locations { Id = 1, Name = "Alpha Site", City = "Austin", State = "TX" },
|
||||
new Locations { Id = 2, Name = "Beta Site", City = "Dallas", State = "TX" },
|
||||
new Locations { Id = 3, Name = "Gamma Yard", City = "Kansas City", State = "MO" },
|
||||
new Locations { Id = 4, Name = "Delta Hub", City = "Indianapolis", State = "indiana" },
|
||||
new Locations { Id = 5, Name = "Epsilon Depot", City = "Houston", State = "TX" },
|
||||
new Locations { Id = 6, Name = "Zeta Lot", City = "Nowhere", State = null });
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetListPagedAsync_WithoutStates_ReturnsAllRows()
|
||||
{
|
||||
await using var ctx = NewContext();
|
||||
await SeedAsync(ctx);
|
||||
|
||||
var (items, totalCount) = await new LocationDataService(ctx)
|
||||
.GetListPagedAsync(1, 10, null, null, CancellationToken.None);
|
||||
|
||||
totalCount.Should().Be(6);
|
||||
items.Should().HaveCount(6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetListPagedAsync_AppliesStatesFilterBeforeCountAndPaging()
|
||||
{
|
||||
await using var ctx = NewContext();
|
||||
await SeedAsync(ctx);
|
||||
|
||||
var (page1, totalCount) = await new LocationDataService(ctx)
|
||||
.GetListPagedAsync(1, 2, null, new[] { "TX" }, CancellationToken.None);
|
||||
|
||||
totalCount.Should().Be(3, "count must reflect the state filter, not the whole table");
|
||||
page1.Should().HaveCount(2);
|
||||
page1.Select(l => l.Name).Should().ContainInOrder("Alpha Site", "Beta Site");
|
||||
|
||||
var (page2, totalCountAgain) = await new LocationDataService(ctx)
|
||||
.GetListPagedAsync(2, 2, null, new[] { "TX" }, CancellationToken.None);
|
||||
|
||||
totalCountAgain.Should().Be(3);
|
||||
page2.Select(l => l.Name).Should().ContainSingle("Epsilon Depot");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetListPagedAsync_MultipleStates_KeepSearchFilter()
|
||||
{
|
||||
await using var ctx = NewContext();
|
||||
await SeedAsync(ctx);
|
||||
|
||||
var (items, totalCount) = await new LocationDataService(ctx)
|
||||
.GetListPagedAsync(1, 10, "Dallas", new[] { "TX", "MO" }, CancellationToken.None);
|
||||
|
||||
totalCount.Should().Be(1);
|
||||
items.Should().ContainSingle().Which.Name.Should().Be("Beta Site");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetListPagedAsync_StatesFilter_ExcludesNullAndNonMatchingStates()
|
||||
{
|
||||
await using var ctx = NewContext();
|
||||
await SeedAsync(ctx);
|
||||
|
||||
var (items, totalCount) = await new LocationDataService(ctx)
|
||||
.GetListPagedAsync(1, 10, null, new[] { "IN", "INDIANA", "MO", "MISSOURI" }, CancellationToken.None);
|
||||
|
||||
totalCount.Should().Be(2);
|
||||
items.Select(l => l.State).Should().BeEquivalentTo("indiana", "MO");
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ public class LocationServiceTests
|
|||
SeedLocation(ctx, "Beta Site", "Dallas");
|
||||
SeedLocation(ctx, "Gamma Yard", "Austin");
|
||||
|
||||
var page = await NewService(ctx).GetLocationListPagedAsync(1, 10, "Austin", CancellationToken.None);
|
||||
var page = await NewService(ctx).GetLocationListPagedAsync(1, 10, "Austin", cancellationToken: CancellationToken.None);
|
||||
|
||||
page.Items.Should().HaveCount(2);
|
||||
page.TotalCount.Should().Be(2);
|
||||
|
|
@ -126,6 +126,93 @@ public class LocationServiceTests
|
|||
page.Items.Select(l => l.Name).Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLocationListPagedAsync_NormalizesStates_CaseWhitespaceAndDuplicates()
|
||||
{
|
||||
var data = new Mock<ILocationDataService>();
|
||||
IReadOnlyCollection<string>? captured = default;
|
||||
data.Setup(d => d.GetListPagedAsync(1, 10, null, It.IsAny<IReadOnlyCollection<string>?>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<int, int, string?, IReadOnlyCollection<string>?, CancellationToken>((_, _, _, states, _) => captured = states)
|
||||
.ReturnsAsync((new List<Locations>(), 0));
|
||||
|
||||
await NewServiceWithSpy(data.Object).GetLocationListPagedAsync(1, 10, null, " tx , Mo ,,TX, ", CancellationToken.None);
|
||||
|
||||
captured.Should().NotBeNull();
|
||||
captured.Should().BeEquivalentTo(new[] { "TX", "TEXAS", "MO", "MISSOURI" });
|
||||
captured.Should().HaveCount(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLocationListPagedAsync_EmptyOrWhitespaceStates_PassesNoStateFilter()
|
||||
{
|
||||
foreach (var states in new[] { null, "", " ", " , , " })
|
||||
{
|
||||
var data = new Mock<ILocationDataService>();
|
||||
IReadOnlyCollection<string>? captured = default;
|
||||
data.Setup(d => d.GetListPagedAsync(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string?>(), It.IsAny<IReadOnlyCollection<string>?>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<int, int, string?, IReadOnlyCollection<string>?, CancellationToken>((_, _, _, stateFilter, _) => captured = stateFilter)
|
||||
.ReturnsAsync((new List<Locations>(), 0));
|
||||
|
||||
var page = await NewServiceWithSpy(data.Object).GetLocationListPagedAsync(1, 10, "search", states, CancellationToken.None);
|
||||
|
||||
captured.Should().BeNull($"states input '{states}' must mean no filter");
|
||||
page.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLocationListPagedAsync_InvalidStates_ThrowsValidationAndNeverQueries()
|
||||
{
|
||||
var data = new Mock<ILocationDataService>();
|
||||
|
||||
var act = () => NewServiceWithSpy(data.Object).GetLocationListPagedAsync(1, 10, null, "TX, zz, QQ", CancellationToken.None);
|
||||
|
||||
(await act.Should().ThrowAsync<FluentValidation.ValidationException>())
|
||||
.Which.Errors.Should().ContainSingle(e =>
|
||||
e.PropertyName == "states"
|
||||
&& e.ErrorMessage.Contains("ZZ")
|
||||
&& e.ErrorMessage.Contains("QQ")
|
||||
&& !e.ErrorMessage.Contains("TX"));
|
||||
data.Verify(d => d.GetListPagedAsync(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string?>(), It.IsAny<IReadOnlyCollection<string>?>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLocationListPagedAsync_AcceptsAllFiftyStateCodes()
|
||||
{
|
||||
var data = new Mock<ILocationDataService>();
|
||||
data.Setup(d => d.GetListPagedAsync(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string?>(), It.IsAny<IReadOnlyCollection<string>?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((new List<Locations>(), 0));
|
||||
|
||||
var allStates = string.Join(",", SeaHaven.Services.Helpers.UsStateCodes.All);
|
||||
|
||||
var act = () => NewServiceWithSpy(data.Object).GetLocationListPagedAsync(1, 10, null, allStates, CancellationToken.None);
|
||||
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLocationListPagedAsync_AppliesNormalizedStatesThroughDataService()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
SeedLocation(ctx, "Alpha Site", "Austin").State = "TX";
|
||||
SeedLocation(ctx, "Beta Site", "Dallas").State = "TX";
|
||||
SeedLocation(ctx, "Gamma Yard", "Kansas City").State = "MO";
|
||||
SeedLocation(ctx, "Delta Hub", "Indianapolis").State = "IN";
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var page = await NewService(ctx).GetLocationListPagedAsync(1, 10, null, " mo , tx ", CancellationToken.None);
|
||||
|
||||
page.TotalCount.Should().Be(3);
|
||||
page.Items.Select(l => l.Name).Should().BeEquivalentTo("Alpha Site", "Beta Site", "Gamma Yard");
|
||||
}
|
||||
|
||||
private static LocationService NewServiceWithSpy(ILocationDataService dataService) =>
|
||||
new(
|
||||
dataService,
|
||||
Mock.Of<IAccountDataService>(),
|
||||
new CreateLocationValidation(),
|
||||
new UpdateLocationValidation());
|
||||
|
||||
[Fact]
|
||||
public async Task GetLocationDetailAsync_ReturnsMappedDtoOrNull()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,38 +36,46 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
}
|
||||
|
||||
[HttpGet("GetLocationList")]
|
||||
public async Task<IActionResult> GetLocationList(string? search = "", int page = 1, int pageSize = 10, CancellationToken cancellationToken = default)
|
||||
public async Task<IActionResult> GetLocationList(string? search = "", int page = 1, int pageSize = 10, string? states = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pagedResult = await _locationService.GetLocationListPagedAsync(page, pageSize, search, cancellationToken);
|
||||
|
||||
var data = pagedResult.Items.Select(l => new
|
||||
try
|
||||
{
|
||||
Id = l.Id,
|
||||
Name = l.Name,
|
||||
Title = l.Title,
|
||||
Address = l.Address1,
|
||||
Address1 = l.Address1,
|
||||
Address2 = l.Address2,
|
||||
City = l.City,
|
||||
State = l.State,
|
||||
ZipCode = l.Zip,
|
||||
Phone = l.PhoneNumber,
|
||||
Contact = (string?)null,
|
||||
ContactEmail = l.Email,
|
||||
Status = l.Status,
|
||||
AccountId = l.AccountId
|
||||
});
|
||||
var pagedResult = await _locationService.GetLocationListPagedAsync(page, pageSize, search, states, cancellationToken);
|
||||
|
||||
var viewModel = new Pagination_DTO
|
||||
var data = pagedResult.Items.Select(l => new
|
||||
{
|
||||
Id = l.Id,
|
||||
Name = l.Name,
|
||||
Title = l.Title,
|
||||
Address = l.Address1,
|
||||
Address1 = l.Address1,
|
||||
Address2 = l.Address2,
|
||||
City = l.City,
|
||||
State = l.State,
|
||||
ZipCode = l.Zip,
|
||||
Phone = l.PhoneNumber,
|
||||
Contact = (string?)null,
|
||||
ContactEmail = l.Email,
|
||||
Status = l.Status,
|
||||
AccountId = l.AccountId
|
||||
});
|
||||
|
||||
var viewModel = new Pagination_DTO
|
||||
{
|
||||
Data = data,
|
||||
PageNumber = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = pagedResult.TotalCount,
|
||||
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
|
||||
};
|
||||
|
||||
return Ok(viewModel);
|
||||
}
|
||||
catch (ValidationException vex)
|
||||
{
|
||||
Data = data,
|
||||
PageNumber = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = pagedResult.TotalCount,
|
||||
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
|
||||
};
|
||||
|
||||
return Ok(viewModel);
|
||||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ namespace SeaHaven.DataServices.Implementation
|
|||
int page,
|
||||
int pageSize,
|
||||
string? search,
|
||||
IReadOnlyCollection<string>? states,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.Locations.AsNoTracking();
|
||||
|
|
@ -146,6 +147,11 @@ namespace SeaHaven.DataServices.Implementation
|
|||
l.City!.Contains(search));
|
||||
}
|
||||
|
||||
if (states is { Count: > 0 })
|
||||
{
|
||||
query = query.Where(l => l.State != null && states.Contains(l.State.Trim().ToUpper()));
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
.OrderBy(l => l.Name)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
Task<bool> ExistsAsync(int id);
|
||||
Task<int> CountAsync();
|
||||
|
||||
Task<(List<Locations> Items, int TotalCount)> GetListPagedAsync(int page, int pageSize, string? search, CancellationToken cancellationToken);
|
||||
Task<(List<Locations> Items, int TotalCount)> GetListPagedAsync(int page, int pageSize, string? search, IReadOnlyCollection<string>? states, CancellationToken cancellationToken);
|
||||
Task<Locations?> GetDetailByIdAsync(int id, CancellationToken cancellationToken);
|
||||
Task<Locations?> GetByIdForUpdateAsync(int id, CancellationToken cancellationToken);
|
||||
Task<Locations> AddAsync(Locations location, CancellationToken cancellationToken);
|
||||
|
|
|
|||
68
SeaHaven.Services/Helpers/UsStateCodes.cs
Normal file
68
SeaHaven.Services/Helpers/UsStateCodes.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class UsStateCodes
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string> Names = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["AL"] = "ALABAMA",
|
||||
["AK"] = "ALASKA",
|
||||
["AZ"] = "ARIZONA",
|
||||
["AR"] = "ARKANSAS",
|
||||
["CA"] = "CALIFORNIA",
|
||||
["CO"] = "COLORADO",
|
||||
["CT"] = "CONNECTICUT",
|
||||
["DE"] = "DELAWARE",
|
||||
["FL"] = "FLORIDA",
|
||||
["GA"] = "GEORGIA",
|
||||
["HI"] = "HAWAII",
|
||||
["ID"] = "IDAHO",
|
||||
["IL"] = "ILLINOIS",
|
||||
["IN"] = "INDIANA",
|
||||
["IA"] = "IOWA",
|
||||
["KS"] = "KANSAS",
|
||||
["KY"] = "KENTUCKY",
|
||||
["LA"] = "LOUISIANA",
|
||||
["ME"] = "MAINE",
|
||||
["MD"] = "MARYLAND",
|
||||
["MA"] = "MASSACHUSETTS",
|
||||
["MI"] = "MICHIGAN",
|
||||
["MN"] = "MINNESOTA",
|
||||
["MS"] = "MISSISSIPPI",
|
||||
["MO"] = "MISSOURI",
|
||||
["MT"] = "MONTANA",
|
||||
["NE"] = "NEBRASKA",
|
||||
["NV"] = "NEVADA",
|
||||
["NH"] = "NEW HAMPSHIRE",
|
||||
["NJ"] = "NEW JERSEY",
|
||||
["NM"] = "NEW MEXICO",
|
||||
["NY"] = "NEW YORK",
|
||||
["NC"] = "NORTH CAROLINA",
|
||||
["ND"] = "NORTH DAKOTA",
|
||||
["OH"] = "OHIO",
|
||||
["OK"] = "OKLAHOMA",
|
||||
["OR"] = "OREGON",
|
||||
["PA"] = "PENNSYLVANIA",
|
||||
["RI"] = "RHODE ISLAND",
|
||||
["SC"] = "SOUTH CAROLINA",
|
||||
["SD"] = "SOUTH DAKOTA",
|
||||
["TN"] = "TENNESSEE",
|
||||
["TX"] = "TEXAS",
|
||||
["UT"] = "UTAH",
|
||||
["VT"] = "VERMONT",
|
||||
["VA"] = "VIRGINIA",
|
||||
["WA"] = "WASHINGTON",
|
||||
["WV"] = "WEST VIRGINIA",
|
||||
["WI"] = "WISCONSIN",
|
||||
["WY"] = "WYOMING"
|
||||
};
|
||||
|
||||
public static IReadOnlySet<string> All { get; } = new HashSet<string>(Names.Keys, StringComparer.Ordinal);
|
||||
|
||||
public static IReadOnlyCollection<string> ExpandStorageValues(IEnumerable<string> codes)
|
||||
{
|
||||
return codes
|
||||
.SelectMany(code => new[] { code, Names[code] })
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -165,9 +165,12 @@ namespace SeaHaven.Services.Implementation
|
|||
int page,
|
||||
int pageSize,
|
||||
string? search,
|
||||
CancellationToken cancellationToken)
|
||||
string? states = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var (items, totalCount) = await _locationDataService.GetListPagedAsync(page, pageSize, search, cancellationToken);
|
||||
var stateFilter = NormalizeStateFilter(states);
|
||||
var storageStateFilter = stateFilter == null ? null : UsStateCodes.ExpandStorageValues(stateFilter);
|
||||
var (items, totalCount) = await _locationDataService.GetListPagedAsync(page, pageSize, search, storageStateFilter, cancellationToken);
|
||||
return new PagedResult<LocationDTO>
|
||||
{
|
||||
Items = items.Select(MapToDTO).ToList(),
|
||||
|
|
@ -177,6 +180,32 @@ namespace SeaHaven.Services.Implementation
|
|||
};
|
||||
}
|
||||
|
||||
internal static IReadOnlyCollection<string>? NormalizeStateFilter(string? states)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(states))
|
||||
return null;
|
||||
|
||||
var normalized = states
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(code => code.ToUpperInvariant())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
if (normalized.Count == 0)
|
||||
return null;
|
||||
|
||||
var invalid = normalized.Where(code => !UsStateCodes.All.Contains(code)).ToList();
|
||||
if (invalid.Count > 0)
|
||||
{
|
||||
throw new ValidationException(new[]
|
||||
{
|
||||
new ValidationFailure("states", $"states must be valid US postal abbreviations: {string.Join(", ", invalid)}.")
|
||||
});
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public async Task<LocationDTO?> GetLocationDetailAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var location = await _locationDataService.GetDetailByIdAsync(id, cancellationToken);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace SeaHaven.Services.Interfaces
|
|||
Task<bool> LocationExistsAsync(int id);
|
||||
Task<int> GetTotalLocationCountAsync();
|
||||
|
||||
Task<PagedResult<LocationDTO>> GetLocationListPagedAsync(int page, int pageSize, string? search, CancellationToken cancellationToken);
|
||||
Task<PagedResult<LocationDTO>> GetLocationListPagedAsync(int page, int pageSize, string? search, string? states = null, CancellationToken cancellationToken = default);
|
||||
Task<LocationDTO?> GetLocationDetailAsync(int id, CancellationToken cancellationToken);
|
||||
Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, ClaimsPrincipal user, CancellationToken cancellationToken);
|
||||
Task UpdateLocationFromRequestAsync(int id, LocationUpdateRequestDTO request, ClaimsPrincipal user, CancellationToken cancellationToken);
|
||||
|
|
|
|||
|
|
@ -835,8 +835,8 @@ public class WorkOrderAccountScopeTests
|
|||
public Task<bool> ExistsAsync(int id) => _inner.ExistsAsync(id);
|
||||
public Task<int> CountAsync() => _inner.CountAsync();
|
||||
public Task<(List<Locations> Items, int TotalCount)> GetListPagedAsync(
|
||||
int page, int pageSize, string? search, CancellationToken cancellationToken)
|
||||
=> _inner.GetListPagedAsync(page, pageSize, search, cancellationToken);
|
||||
int page, int pageSize, string? search, IReadOnlyCollection<string>? states, CancellationToken cancellationToken)
|
||||
=> _inner.GetListPagedAsync(page, pageSize, search, states, cancellationToken);
|
||||
public Task<Locations?> GetDetailByIdAsync(int id, CancellationToken cancellationToken)
|
||||
=> _inner.GetDetailByIdAsync(id, cancellationToken);
|
||||
public Task<Locations?> GetByIdForUpdateAsync(int id, CancellationToken cancellationToken)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue