fix(dashboard): resolve region zones from full state names (SH-348)

Locations store State as either a two-letter postal code or a full state
name (the location list filter expands codes to both forms via
UsStateCodes.ExpandStorageValues, and dev fixtures carry State = "indiana").
DashboardRegions.Resolve only matched the two-letter key, so every location
stored as a full name fell into Unmapped/Other and the East/Central/West/
California bars undercounted.

Normalise the stored value through a new UsStateCodes.ToCode, which accepts a
code or a full name and returns the canonical postal code, before the zone
lookup. Extend the region test with full-name storage forms and add an
invariant asserting every US state and its full-name form resolves to a
canonical bucket.
This commit is contained in:
Alexandre Brandizzi 2026-09-16 20:15:20 -03:00
parent d4afcced87
commit 22308f21f6
3 changed files with 53 additions and 4 deletions

View file

@ -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");
}
}
}

View file

@ -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";
}

View file

@ -56,8 +56,29 @@ namespace SeaHaven.Services.Helpers
["WY"] = "WYOMING"
};
private static readonly IReadOnlyDictionary<string, string> CodesByName =
Names.ToDictionary(pair => pair.Value, pair => pair.Key, StringComparer.Ordinal);
public static IReadOnlySet<string> All { get; } = new HashSet<string>(Names.Keys, StringComparer.Ordinal);
/// <summary>
/// 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 <see cref="ExpandStorageValues"/>. Returns null when
/// the value is blank or is not a recognised US state.
/// </summary>
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<string> ExpandStorageValues(IEnumerable<string> codes)
{
return codes