diff --git a/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs b/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs index c7c6410..4b6b2c5 100644 --- a/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs +++ b/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs @@ -309,11 +309,17 @@ public class DashboardServiceTests ctx.Locations.AddRange( new Locations { Id = 1, State = "NY" }, new Locations { Id = 2, State = "ca" }, - new Locations { Id = 3, State = "XX" }); + new Locations { Id = 3, State = "XX" }, + new Locations { Id = 4, State = "indiana" }, + new Locations { Id = 5, State = "NEW YORK" }, + new Locations { Id = 6, State = "California" }); ctx.workOrders.AddRange( new WorkOrder { AccountId = 1, LocationId = 1 }, new WorkOrder { AccountId = 1, LocationId = 2 }, new WorkOrder { AccountId = 1, LocationId = 3 }, + new WorkOrder { AccountId = 1, LocationId = 4 }, + new WorkOrder { AccountId = 1, LocationId = 5 }, + new WorkOrder { AccountId = 1, LocationId = 6 }, new WorkOrder { AccountId = 1, LocationId = null }); await ctx.SaveChangesAsync(); @@ -322,6 +328,27 @@ public class DashboardServiceTests result.Items.Select(row => row.Region).Should().Equal( "East", "Central", "West", "California", "Unmapped/Other"); - result.Items.Select(row => row.WorkOrderCount).Should().Equal(1, 0, 0, 1, 2); + // NY + "NEW YORK" -> East; "indiana" -> Central; "ca" + "California" -> California; + // "XX" + unassigned location -> Unmapped/Other. Full-name storage forms must not + // fall through to Unmapped/Other (SH-348 review). + result.Items.Select(row => row.WorkOrderCount).Should().Equal(2, 1, 0, 2, 2); + } + + [Fact] + public void Resolve_MapsEveryUsStateAndItsFullNameToACanonicalBucket() + { + foreach (var code in UsStateCodes.All) + { + var byCode = DashboardRegions.Resolve(code); + byCode.Should().NotBe( + "Unmapped/Other", + $"state code {code} must belong to a canonical region"); + + var fullName = UsStateCodes.ExpandStorageValues(new[] { code }) + .First(value => value != code); + DashboardRegions.Resolve(fullName).Should().Be( + byCode, + $"the full-name storage form of {code} must resolve to the same region as the code"); + } } } diff --git a/SeaHaven.Services/Helpers/DashboardRegions.cs b/SeaHaven.Services/Helpers/DashboardRegions.cs index f3b5fae..d41ae83 100644 --- a/SeaHaven.Services/Helpers/DashboardRegions.cs +++ b/SeaHaven.Services/Helpers/DashboardRegions.cs @@ -9,10 +9,11 @@ public static class DashboardRegions public static string Resolve(string? state) { - if (string.IsNullOrWhiteSpace(state)) + var code = UsStateCodes.ToCode(state); + if (code is null) return "Unmapped/Other"; - return StateToRegion.TryGetValue(state.Trim().ToUpperInvariant(), out var region) + return StateToRegion.TryGetValue(code, out var region) ? region : "Unmapped/Other"; } diff --git a/SeaHaven.Services/Helpers/UsStateCodes.cs b/SeaHaven.Services/Helpers/UsStateCodes.cs index 9d64a1a..54c32ea 100644 --- a/SeaHaven.Services/Helpers/UsStateCodes.cs +++ b/SeaHaven.Services/Helpers/UsStateCodes.cs @@ -56,8 +56,29 @@ namespace SeaHaven.Services.Helpers ["WY"] = "WYOMING" }; + private static readonly IReadOnlyDictionary CodesByName = + Names.ToDictionary(pair => pair.Value, pair => pair.Key, StringComparer.Ordinal); + public static IReadOnlySet All { get; } = new HashSet(Names.Keys, StringComparer.Ordinal); + /// + /// Normalises a stored state value to its two-letter postal code. Accepts either a + /// postal code ("IN") or a full state name ("Indiana"), case-insensitively, matching + /// the storage forms produced by . Returns null when + /// the value is blank or is not a recognised US state. + /// + public static string? ToCode(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + var normalized = value.Trim().ToUpperInvariant(); + if (All.Contains(normalized)) + return normalized; + + return CodesByName.TryGetValue(normalized, out var code) ? code : null; + } + public static IReadOnlyCollection ExpandStorageValues(IEnumerable codes) { return codes