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