You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

135 lines
5.6KB

  1. using System.Collections.Concurrent;
  2. using Campaign_Tracker.Server.ExtensionData;
  3. using Campaign_Tracker.Server.LegacyData;
  4. namespace Campaign_Tracker.Server.Municipalities;
  5. /// <summary>
  6. /// In-memory municipality profile store for development and integration testing.
  7. /// Implements <see cref="ILegacyLinkedRecordProvider"/> so profiles are included in
  8. /// the nightly extension-to-legacy link integrity check (Story 1.8 AC #4).
  9. /// </summary>
  10. public sealed class InMemoryMunicipalityProfileRepository
  11. : IMunicipalityProfileRepository, ILegacyLinkedRecordProvider
  12. {
  13. private readonly ConcurrentDictionary<string, MunicipalityProfile> _profiles = new(StringComparer.OrdinalIgnoreCase);
  14. private readonly ILegacyLinkValidator _validator;
  15. private readonly ILegacyDataAccess _legacyData;
  16. private readonly TimeProvider _timeProvider;
  17. public InMemoryMunicipalityProfileRepository(
  18. ILegacyLinkValidator validator,
  19. ILegacyDataAccess legacyData,
  20. TimeProvider timeProvider)
  21. {
  22. _validator = validator;
  23. _legacyData = legacyData;
  24. _timeProvider = timeProvider;
  25. }
  26. // ── AC #1: create with required JCode link ────────────────────────────────
  27. public async Task<MunicipalityProfileSaveResult> CreateAsync(
  28. string jCode,
  29. string? displayName,
  30. string actorIdentity,
  31. CancellationToken cancellationToken = default)
  32. {
  33. if (string.IsNullOrWhiteSpace(jCode))
  34. return MunicipalityProfileSaveResult.Failure("JCode is required.");
  35. // AC #4: validate before saving; never write if the link is invalid
  36. var linkRef = LegacyLinkReference.ForJurisdiction(jCode);
  37. var validation = await _validator.ValidateAsync(linkRef, cancellationToken);
  38. if (!validation.IsValid)
  39. return MunicipalityProfileSaveResult.Failure(validation.Error!);
  40. // Each JCode maps to exactly one municipality profile
  41. if (_profiles.Values.Any(p => string.Equals(p.JCode, jCode, StringComparison.OrdinalIgnoreCase)))
  42. return MunicipalityProfileSaveResult.Failure(
  43. $"A municipality profile already exists for JCode '{jCode}'.");
  44. var now = _timeProvider.GetUtcNow();
  45. var profile = new MunicipalityProfile(
  46. ProfileId: Guid.NewGuid().ToString("N"),
  47. JCode: jCode.Trim().ToUpperInvariant(),
  48. DisplayName: string.IsNullOrWhiteSpace(displayName) ? null : displayName.Trim(),
  49. CreatedAt: now,
  50. UpdatedAt: now,
  51. UpdatedBy: actorIdentity);
  52. _profiles[profile.ProfileId] = profile;
  53. return MunicipalityProfileSaveResult.Success(profile);
  54. }
  55. // ── AC #3: update with audit trail captured by caller ────────────────────
  56. public Task<MunicipalityProfileSaveResult> UpdateAsync(
  57. string profileId,
  58. string? displayName,
  59. string actorIdentity,
  60. CancellationToken cancellationToken = default)
  61. {
  62. if (!_profiles.TryGetValue(profileId, out var existing))
  63. return Task.FromResult(MunicipalityProfileSaveResult.Failure(
  64. $"Municipality profile '{profileId}' not found."));
  65. var updated = existing with
  66. {
  67. DisplayName = string.IsNullOrWhiteSpace(displayName) ? null : displayName.Trim(),
  68. UpdatedAt = _timeProvider.GetUtcNow(),
  69. UpdatedBy = actorIdentity,
  70. };
  71. _profiles[profileId] = updated;
  72. return Task.FromResult(MunicipalityProfileSaveResult.Success(updated));
  73. }
  74. // ── AC #2: resolve combined extension + legacy view ──────────────────────
  75. public async Task<MunicipalityProfileView?> GetByIdAsync(
  76. string profileId,
  77. CancellationToken cancellationToken = default)
  78. {
  79. if (!_profiles.TryGetValue(profileId, out var profile))
  80. return null;
  81. return await BuildViewAsync(profile, cancellationToken);
  82. }
  83. public async Task<IReadOnlyList<MunicipalityProfileView>> GetAllAsync(
  84. CancellationToken cancellationToken = default)
  85. {
  86. var profiles = _profiles.Values
  87. .OrderBy(p => p.JCode, StringComparer.OrdinalIgnoreCase)
  88. .ToArray();
  89. var views = new List<MunicipalityProfileView>(profiles.Length);
  90. foreach (var profile in profiles)
  91. views.Add(await BuildViewAsync(profile, cancellationToken));
  92. return views;
  93. }
  94. // ── ILegacyLinkedRecordProvider ───────────────────────────────────────────
  95. Task<IReadOnlyList<ILegacyLinkedRecord>> ILegacyLinkedRecordProvider.GetAllAsync(
  96. CancellationToken cancellationToken)
  97. => Task.FromResult<IReadOnlyList<ILegacyLinkedRecord>>(
  98. _profiles.Values.Cast<ILegacyLinkedRecord>().ToArray());
  99. // ── helpers ───────────────────────────────────────────────────────────────
  100. private async Task<MunicipalityProfileView> BuildViewAsync(
  101. MunicipalityProfile profile,
  102. CancellationToken cancellationToken)
  103. {
  104. var jurisdiction = await _legacyData.GetJurisdictionAsync(profile.JCode, cancellationToken);
  105. return new MunicipalityProfileView(
  106. Profile: profile,
  107. LegacyName: jurisdiction?.Name,
  108. LegacyMailingAddress: jurisdiction?.MailingAddress,
  109. LegacyCityStateZip: jurisdiction?.CityStateZip);
  110. }
  111. }

Powered by TurnKey Linux.