|
- using System.Collections.Concurrent;
-
- namespace Campaign_Tracker.Server.ElectionCycles;
-
- public sealed class InMemoryElectionCycleJobRepository : IElectionCycleJobRepository
- {
- private const string StatusInSetup = "In Setup";
-
- private static readonly Dictionary<string, int> StatusSortOrder =
- new(StringComparer.OrdinalIgnoreCase)
- {
- [StatusInSetup] = 0,
- ["Ready"] = 1,
- ["In progress"] = 2,
- ["At risk"] = 3,
- ["Complete"] = 4,
- ["Blocked"] = 5,
- };
-
- private readonly ConcurrentDictionary<string, ElectionCycleJob> _jobs =
- new(StringComparer.OrdinalIgnoreCase);
- private readonly TimeProvider _timeProvider;
-
- // Seed data for kanban read model compatibility (Story 2.1).
- private readonly List<ElectionCycleJobAssignment> _seedAssignments =
- [
- new(
- JobId: "job-fair01-primary",
- JCode: "FAIR01",
- CycleId: "2026-primary",
- CycleName: "2026 Primary",
- Status: "Ready",
- IsActive: true),
- new(
- JobId: "job-lake02-primary",
- JCode: "LAKE02",
- CycleId: "2026-primary",
- CycleName: "2026 Primary",
- Status: "In progress",
- IsActive: true),
- new(
- JobId: "job-lake02-special",
- JCode: "LAKE02",
- CycleId: "2026-special",
- CycleName: "2026 Special",
- Status: "At risk",
- IsActive: true),
- new(
- JobId: "job-pine03-2024",
- JCode: "PINE03",
- CycleId: "2024-general",
- CycleName: "2024 General",
- Status: "Complete",
- IsActive: false),
- ];
-
- public InMemoryElectionCycleJobRepository(TimeProvider timeProvider)
- {
- _timeProvider = timeProvider;
- }
-
- // Constructor for tests that provide pre-seeded assignments.
- // Clears default seed data so tests control the exact dataset.
- public InMemoryElectionCycleJobRepository(
- IReadOnlyList<ElectionCycleJobAssignment> additionalAssignments,
- TimeProvider? timeProvider = null)
- {
- _timeProvider = timeProvider ?? TimeProvider.System;
- _seedAssignments.Clear();
- foreach (var a in additionalAssignments)
- {
- var job = new ElectionCycleJob(
- JobId: a.JobId,
- JCode: a.JCode,
- CycleId: a.CycleId,
- CycleName: a.CycleName,
- Status: a.Status,
- CreatedBy: "seed",
- CreatedAt: _timeProvider.GetUtcNow());
- _jobs[job.JobId] = job;
- _seedAssignments.Add(a);
- }
- }
-
- public Task<IReadOnlyList<ElectionCycleJobAssignment>> GetAllAsync(
- CancellationToken cancellationToken = default)
- {
- // Return seed assignments plus any dynamically created jobs not already in seeds.
- var jobIds = _seedAssignments.Select(a => a.JobId).ToHashSet(StringComparer.OrdinalIgnoreCase);
- var dynamicJobs = _jobs.Values
- .Where(j => !jobIds.Contains(j.JobId))
- .Select(MapToAssignment)
- .ToArray();
- var all = _seedAssignments.Concat(dynamicJobs).ToArray();
- return Task.FromResult<IReadOnlyList<ElectionCycleJobAssignment>>(all);
- }
-
- public Task<ElectionCycleJobSaveResult> CreateAsync(
- string jCode,
- string cycleId,
- string cycleName,
- string actorIdentity,
- CancellationToken cancellationToken = default)
- {
- var error = Validate(jCode, cycleId, cycleName);
- if (error is not null)
- return Task.FromResult(ElectionCycleJobSaveResult.Failure(error));
-
- var now = _timeProvider.GetUtcNow();
- var jobId = $"job-{jCode.ToLowerInvariant()}-{cycleId.ToLowerInvariant()}";
-
- // Idempotency: if a job with this composite key already exists, return it.
- if (_jobs.TryGetValue(jobId, out var existing))
- return Task.FromResult(ElectionCycleJobSaveResult.Success(existing));
-
- var job = new ElectionCycleJob(
- JobId: jobId,
- JCode: jCode.Trim().ToUpperInvariant(),
- CycleId: cycleId.Trim(),
- CycleName: cycleName.Trim(),
- Status: StatusInSetup,
- CreatedBy: actorIdentity,
- CreatedAt: now);
-
- _jobs[jobId] = job;
- return Task.FromResult(ElectionCycleJobSaveResult.Success(job));
- }
-
- private static string? Validate(string jCode, string cycleId, string cycleName)
- {
- if (string.IsNullOrWhiteSpace(jCode))
- return "Municipality identifier (JCode) is required.";
-
- if (string.IsNullOrWhiteSpace(cycleId))
- return "Cycle selection is required.";
-
- if (string.IsNullOrWhiteSpace(cycleName))
- return "Cycle name is required.";
-
- return null;
- }
-
- private static ElectionCycleJobAssignment MapToAssignment(ElectionCycleJob job) =>
- new(
- JobId: job.JobId,
- JCode: job.JCode,
- CycleId: job.CycleId,
- CycleName: job.CycleName,
- Status: job.Status,
- IsActive: true);
- }
|