Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

152 řádky
5.1KB

  1. using System.Collections.Concurrent;
  2. namespace Campaign_Tracker.Server.ElectionCycles;
  3. public sealed class InMemoryElectionCycleJobRepository : IElectionCycleJobRepository
  4. {
  5. private const string StatusInSetup = "In Setup";
  6. private static readonly Dictionary<string, int> StatusSortOrder =
  7. new(StringComparer.OrdinalIgnoreCase)
  8. {
  9. [StatusInSetup] = 0,
  10. ["Ready"] = 1,
  11. ["In progress"] = 2,
  12. ["At risk"] = 3,
  13. ["Complete"] = 4,
  14. ["Blocked"] = 5,
  15. };
  16. private readonly ConcurrentDictionary<string, ElectionCycleJob> _jobs =
  17. new(StringComparer.OrdinalIgnoreCase);
  18. private readonly TimeProvider _timeProvider;
  19. // Seed data for kanban read model compatibility (Story 2.1).
  20. private readonly List<ElectionCycleJobAssignment> _seedAssignments =
  21. [
  22. new(
  23. JobId: "job-fair01-primary",
  24. JCode: "FAIR01",
  25. CycleId: "2026-primary",
  26. CycleName: "2026 Primary",
  27. Status: "Ready",
  28. IsActive: true),
  29. new(
  30. JobId: "job-lake02-primary",
  31. JCode: "LAKE02",
  32. CycleId: "2026-primary",
  33. CycleName: "2026 Primary",
  34. Status: "In progress",
  35. IsActive: true),
  36. new(
  37. JobId: "job-lake02-special",
  38. JCode: "LAKE02",
  39. CycleId: "2026-special",
  40. CycleName: "2026 Special",
  41. Status: "At risk",
  42. IsActive: true),
  43. new(
  44. JobId: "job-pine03-2024",
  45. JCode: "PINE03",
  46. CycleId: "2024-general",
  47. CycleName: "2024 General",
  48. Status: "Complete",
  49. IsActive: false),
  50. ];
  51. public InMemoryElectionCycleJobRepository(TimeProvider timeProvider)
  52. {
  53. _timeProvider = timeProvider;
  54. }
  55. // Constructor for tests that provide pre-seeded assignments.
  56. // Clears default seed data so tests control the exact dataset.
  57. public InMemoryElectionCycleJobRepository(
  58. IReadOnlyList<ElectionCycleJobAssignment> additionalAssignments,
  59. TimeProvider? timeProvider = null)
  60. {
  61. _timeProvider = timeProvider ?? TimeProvider.System;
  62. _seedAssignments.Clear();
  63. foreach (var a in additionalAssignments)
  64. {
  65. var job = new ElectionCycleJob(
  66. JobId: a.JobId,
  67. JCode: a.JCode,
  68. CycleId: a.CycleId,
  69. CycleName: a.CycleName,
  70. Status: a.Status,
  71. CreatedBy: "seed",
  72. CreatedAt: _timeProvider.GetUtcNow());
  73. _jobs[job.JobId] = job;
  74. _seedAssignments.Add(a);
  75. }
  76. }
  77. public Task<IReadOnlyList<ElectionCycleJobAssignment>> GetAllAsync(
  78. CancellationToken cancellationToken = default)
  79. {
  80. // Return seed assignments plus any dynamically created jobs not already in seeds.
  81. var jobIds = _seedAssignments.Select(a => a.JobId).ToHashSet(StringComparer.OrdinalIgnoreCase);
  82. var dynamicJobs = _jobs.Values
  83. .Where(j => !jobIds.Contains(j.JobId))
  84. .Select(MapToAssignment)
  85. .ToArray();
  86. var all = _seedAssignments.Concat(dynamicJobs).ToArray();
  87. return Task.FromResult<IReadOnlyList<ElectionCycleJobAssignment>>(all);
  88. }
  89. public Task<ElectionCycleJobSaveResult> CreateAsync(
  90. string jCode,
  91. string cycleId,
  92. string cycleName,
  93. string actorIdentity,
  94. CancellationToken cancellationToken = default)
  95. {
  96. var error = Validate(jCode, cycleId, cycleName);
  97. if (error is not null)
  98. return Task.FromResult(ElectionCycleJobSaveResult.Failure(error));
  99. var now = _timeProvider.GetUtcNow();
  100. var jobId = $"job-{jCode.ToLowerInvariant()}-{cycleId.ToLowerInvariant()}";
  101. // Idempotency: if a job with this composite key already exists, return it.
  102. if (_jobs.TryGetValue(jobId, out var existing))
  103. return Task.FromResult(ElectionCycleJobSaveResult.Success(existing));
  104. var job = new ElectionCycleJob(
  105. JobId: jobId,
  106. JCode: jCode.Trim().ToUpperInvariant(),
  107. CycleId: cycleId.Trim(),
  108. CycleName: cycleName.Trim(),
  109. Status: StatusInSetup,
  110. CreatedBy: actorIdentity,
  111. CreatedAt: now);
  112. _jobs[jobId] = job;
  113. return Task.FromResult(ElectionCycleJobSaveResult.Success(job));
  114. }
  115. private static string? Validate(string jCode, string cycleId, string cycleName)
  116. {
  117. if (string.IsNullOrWhiteSpace(jCode))
  118. return "Municipality identifier (JCode) is required.";
  119. if (string.IsNullOrWhiteSpace(cycleId))
  120. return "Cycle selection is required.";
  121. if (string.IsNullOrWhiteSpace(cycleName))
  122. return "Cycle name is required.";
  123. return null;
  124. }
  125. private static ElectionCycleJobAssignment MapToAssignment(ElectionCycleJob job) =>
  126. new(
  127. JobId: job.JobId,
  128. JCode: job.JCode,
  129. CycleId: job.CycleId,
  130. CycleName: job.CycleName,
  131. Status: job.Status,
  132. IsActive: true);
  133. }

Powered by TurnKey Linux.