|
- using System.Security.Claims;
- using Campaign_Tracker.Server.Audit;
- using Campaign_Tracker.Server.Authorization;
- using Campaign_Tracker.Server.ElectionCycles;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Mvc;
-
- namespace Campaign_Tracker.Server.Controllers;
-
- [ApiController]
- [Authorize(Policy = ApplicationPolicy.ClientServicesAccess)]
- [Route("api/election-cycles/jobs")]
- public sealed class ElectionCycleJobsController : ControllerBase
- {
- private readonly IElectionCycleJobRepository _jobs;
- private readonly IAuditService _audit;
- private readonly TimeProvider _timeProvider;
-
- public ElectionCycleJobsController(
- IElectionCycleJobRepository jobs,
- IAuditService audit,
- TimeProvider timeProvider)
- {
- _jobs = jobs;
- _audit = audit;
- _timeProvider = timeProvider;
- }
-
- [HttpPost]
- public async Task<ActionResult<ElectionCycleJobResponse>> Create(
- [FromBody] CreateElectionCycleJobRequest request,
- CancellationToken cancellationToken)
- {
- if (string.IsNullOrWhiteSpace(request.JCode))
- return UnprocessableEntity(new ElectionCycleJobProblem("Municipality identifier (JCode) is required."));
-
- if (string.IsNullOrWhiteSpace(request.CycleId) && string.IsNullOrWhiteSpace(request.CycleName))
- return UnprocessableEntity(new ElectionCycleJobProblem("Cycle selection is required — provide an existing cycle or a new cycle name."));
-
- var cycleId = string.IsNullOrWhiteSpace(request.CycleId)
- ? NormalizeNewCycleId(request.CycleName!)
- : request.CycleId.Trim();
- var cycleName = string.IsNullOrWhiteSpace(request.CycleName)
- ? cycleId // fallback — shouldn't happen given validation above
- : request.CycleName.Trim();
-
- var actor = GetActor();
- var result = await _jobs.CreateAsync(
- request.JCode,
- cycleId,
- cycleName,
- actor,
- cancellationToken);
-
- if (!result.Saved || result.Job is null)
- return UnprocessableEntity(new ElectionCycleJobProblem(result.Error ?? "Job creation failed."));
-
- _audit.Record(new AuditEvent(
- EventType: "ELECTION_CYCLE_JOB_CREATED",
- ActorIdentity: actor,
- Resource: $"election-cycles/jobs/{result.Job.JobId}",
- Outcome: $"created job for {request.JCode} in cycle {cycleName}",
- TraceIdentifier: HttpContext.TraceIdentifier,
- RecordedAt: _timeProvider.GetUtcNow()));
-
- return CreatedAtAction(
- nameof(GetById),
- new { jobId = result.Job.JobId },
- ElectionCycleJobResponse.From(result.Job));
- }
-
- [HttpGet("{jobId}")]
- public async Task<ActionResult<ElectionCycleJobResponse>> GetById(
- string jobId,
- CancellationToken cancellationToken)
- {
- var allAssignments = await _jobs.GetAllAsync(cancellationToken);
- var match = allAssignments.FirstOrDefault(a =>
- string.Equals(a.JobId, jobId, StringComparison.OrdinalIgnoreCase));
-
- if (match is null)
- return NotFound();
-
- // For dynamically created jobs we don't have full entity here from assignments;
- // the kanban read model serves as the source of truth for listing.
- // This endpoint returns the assignment data mapped to a response shape.
- return Ok(new ElectionCycleJobResponse(
- JobId: match.JobId,
- JCode: match.JCode,
- CycleId: match.CycleId,
- CycleName: match.CycleName,
- Status: match.Status,
- CreatedBy: "system",
- CreatedAt: DateTimeOffset.UtcNow.ToString("O")));
- }
-
- private static string NormalizeNewCycleId(string cycleName) =>
- cycleName.ToLowerInvariant()
- .Replace(" ", "-")
- .Replace("'", "")
- .Replace(",", "");
-
- private string GetActor() =>
- User.Identity?.Name
- ?? User.FindFirstValue(ClaimTypes.NameIdentifier)
- ?? "unknown";
- }
-
- public sealed record CreateElectionCycleJobRequest(
- string JCode,
- string? CycleId,
- string? CycleName);
-
- public sealed record ElectionCycleJobResponse(
- string JobId,
- string JCode,
- string CycleId,
- string CycleName,
- string Status,
- string CreatedBy,
- string CreatedAt)
- {
- public static ElectionCycleJobResponse From(ElectionCycleJob job) =>
- new(
- JobId: job.JobId,
- JCode: job.JCode,
- CycleId: job.CycleId,
- CycleName: job.CycleName,
- Status: job.Status,
- CreatedBy: job.CreatedBy,
- CreatedAt: job.CreatedAt.ToString("O"));
- }
-
- public sealed record ElectionCycleJobProblem(string Error);
|