using System.Collections.Concurrent;
using Campaign_Tracker.Server.ExtensionData;
using Campaign_Tracker.Server.LegacyData;
namespace Campaign_Tracker.Server.Municipalities;
///
/// In-memory municipality profile store for development and integration testing.
/// Implements so profiles are included in
/// the nightly extension-to-legacy link integrity check (Story 1.8 AC #4).
///
public sealed class InMemoryMunicipalityProfileRepository
: IMunicipalityProfileRepository, ILegacyLinkedRecordProvider
{
private readonly ConcurrentDictionary _profiles = new(StringComparer.OrdinalIgnoreCase);
private readonly ILegacyLinkValidator _validator;
private readonly ILegacyDataAccess _legacyData;
private readonly TimeProvider _timeProvider;
public InMemoryMunicipalityProfileRepository(
ILegacyLinkValidator validator,
ILegacyDataAccess legacyData,
TimeProvider timeProvider)
{
_validator = validator;
_legacyData = legacyData;
_timeProvider = timeProvider;
}
// ── AC #1: create with required JCode link ────────────────────────────────
public async Task CreateAsync(
string jCode,
string? displayName,
string actorIdentity,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(jCode))
return MunicipalityProfileSaveResult.Failure("JCode is required.");
// AC #4: validate before saving; never write if the link is invalid
var linkRef = LegacyLinkReference.ForJurisdiction(jCode);
var validation = await _validator.ValidateAsync(linkRef, cancellationToken);
if (!validation.IsValid)
return MunicipalityProfileSaveResult.Failure(validation.Error!);
// Each JCode maps to exactly one municipality profile
if (_profiles.Values.Any(p => string.Equals(p.JCode, jCode, StringComparison.OrdinalIgnoreCase)))
return MunicipalityProfileSaveResult.Failure(
$"A municipality profile already exists for JCode '{jCode}'.");
var now = _timeProvider.GetUtcNow();
var profile = new MunicipalityProfile(
ProfileId: Guid.NewGuid().ToString("N"),
JCode: jCode.Trim().ToUpperInvariant(),
DisplayName: string.IsNullOrWhiteSpace(displayName) ? null : displayName.Trim(),
CreatedAt: now,
UpdatedAt: now,
UpdatedBy: actorIdentity);
_profiles[profile.ProfileId] = profile;
return MunicipalityProfileSaveResult.Success(profile);
}
// ── AC #3: update with audit trail captured by caller ────────────────────
public Task UpdateAsync(
string profileId,
string? displayName,
string actorIdentity,
CancellationToken cancellationToken = default)
{
if (!_profiles.TryGetValue(profileId, out var existing))
return Task.FromResult(MunicipalityProfileSaveResult.Failure(
$"Municipality profile '{profileId}' not found."));
var updated = existing with
{
DisplayName = string.IsNullOrWhiteSpace(displayName) ? null : displayName.Trim(),
UpdatedAt = _timeProvider.GetUtcNow(),
UpdatedBy = actorIdentity,
};
_profiles[profileId] = updated;
return Task.FromResult(MunicipalityProfileSaveResult.Success(updated));
}
// ── AC #2: resolve combined extension + legacy view ──────────────────────
public async Task GetByIdAsync(
string profileId,
CancellationToken cancellationToken = default)
{
if (!_profiles.TryGetValue(profileId, out var profile))
return null;
return await BuildViewAsync(profile, cancellationToken);
}
public async Task> GetAllAsync(
CancellationToken cancellationToken = default)
{
var profiles = _profiles.Values
.OrderBy(p => p.JCode, StringComparer.OrdinalIgnoreCase)
.ToArray();
var views = new List(profiles.Length);
foreach (var profile in profiles)
views.Add(await BuildViewAsync(profile, cancellationToken));
return views;
}
// ── ILegacyLinkedRecordProvider ───────────────────────────────────────────
Task> ILegacyLinkedRecordProvider.GetAllAsync(
CancellationToken cancellationToken)
=> Task.FromResult>(
_profiles.Values.Cast().ToArray());
// ── helpers ───────────────────────────────────────────────────────────────
private async Task BuildViewAsync(
MunicipalityProfile profile,
CancellationToken cancellationToken)
{
var jurisdiction = await _legacyData.GetJurisdictionAsync(profile.JCode, cancellationToken);
return new MunicipalityProfileView(
Profile: profile,
LegacyName: jurisdiction?.Name,
LegacyMailingAddress: jurisdiction?.MailingAddress,
LegacyCityStateZip: jurisdiction?.CityStateZip);
}
}