Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

135 Zeilen
4.7KB

  1. using System.Security.Claims;
  2. using Campaign_Tracker.Server.Audit;
  3. using Campaign_Tracker.Server.Authorization;
  4. using Campaign_Tracker.Server.ElectionCycles;
  5. using Microsoft.AspNetCore.Authorization;
  6. using Microsoft.AspNetCore.Mvc;
  7. namespace Campaign_Tracker.Server.Controllers;
  8. [ApiController]
  9. [Authorize(Policy = ApplicationPolicy.ClientServicesAccess)]
  10. [Route("api/election-cycles/jobs")]
  11. public sealed class ElectionCycleJobsController : ControllerBase
  12. {
  13. private readonly IElectionCycleJobRepository _jobs;
  14. private readonly IAuditService _audit;
  15. private readonly TimeProvider _timeProvider;
  16. public ElectionCycleJobsController(
  17. IElectionCycleJobRepository jobs,
  18. IAuditService audit,
  19. TimeProvider timeProvider)
  20. {
  21. _jobs = jobs;
  22. _audit = audit;
  23. _timeProvider = timeProvider;
  24. }
  25. [HttpPost]
  26. public async Task<ActionResult<ElectionCycleJobResponse>> Create(
  27. [FromBody] CreateElectionCycleJobRequest request,
  28. CancellationToken cancellationToken)
  29. {
  30. if (string.IsNullOrWhiteSpace(request.JCode))
  31. return UnprocessableEntity(new ElectionCycleJobProblem("Municipality identifier (JCode) is required."));
  32. if (string.IsNullOrWhiteSpace(request.CycleId) && string.IsNullOrWhiteSpace(request.CycleName))
  33. return UnprocessableEntity(new ElectionCycleJobProblem("Cycle selection is required — provide an existing cycle or a new cycle name."));
  34. var cycleId = string.IsNullOrWhiteSpace(request.CycleId)
  35. ? NormalizeNewCycleId(request.CycleName!)
  36. : request.CycleId.Trim();
  37. var cycleName = string.IsNullOrWhiteSpace(request.CycleName)
  38. ? cycleId // fallback — shouldn't happen given validation above
  39. : request.CycleName.Trim();
  40. var actor = GetActor();
  41. var result = await _jobs.CreateAsync(
  42. request.JCode,
  43. cycleId,
  44. cycleName,
  45. actor,
  46. cancellationToken);
  47. if (!result.Saved || result.Job is null)
  48. return UnprocessableEntity(new ElectionCycleJobProblem(result.Error ?? "Job creation failed."));
  49. _audit.Record(new AuditEvent(
  50. EventType: "ELECTION_CYCLE_JOB_CREATED",
  51. ActorIdentity: actor,
  52. Resource: $"election-cycles/jobs/{result.Job.JobId}",
  53. Outcome: $"created job for {request.JCode} in cycle {cycleName}",
  54. TraceIdentifier: HttpContext.TraceIdentifier,
  55. RecordedAt: _timeProvider.GetUtcNow()));
  56. return CreatedAtAction(
  57. nameof(GetById),
  58. new { jobId = result.Job.JobId },
  59. ElectionCycleJobResponse.From(result.Job));
  60. }
  61. [HttpGet("{jobId}")]
  62. public async Task<ActionResult<ElectionCycleJobResponse>> GetById(
  63. string jobId,
  64. CancellationToken cancellationToken)
  65. {
  66. var allAssignments = await _jobs.GetAllAsync(cancellationToken);
  67. var match = allAssignments.FirstOrDefault(a =>
  68. string.Equals(a.JobId, jobId, StringComparison.OrdinalIgnoreCase));
  69. if (match is null)
  70. return NotFound();
  71. // For dynamically created jobs we don't have full entity here from assignments;
  72. // the kanban read model serves as the source of truth for listing.
  73. // This endpoint returns the assignment data mapped to a response shape.
  74. return Ok(new ElectionCycleJobResponse(
  75. JobId: match.JobId,
  76. JCode: match.JCode,
  77. CycleId: match.CycleId,
  78. CycleName: match.CycleName,
  79. Status: match.Status,
  80. CreatedBy: "system",
  81. CreatedAt: DateTimeOffset.UtcNow.ToString("O")));
  82. }
  83. private static string NormalizeNewCycleId(string cycleName) =>
  84. cycleName.ToLowerInvariant()
  85. .Replace(" ", "-")
  86. .Replace("'", "")
  87. .Replace(",", "");
  88. private string GetActor() =>
  89. User.Identity?.Name
  90. ?? User.FindFirstValue(ClaimTypes.NameIdentifier)
  91. ?? "unknown";
  92. }
  93. public sealed record CreateElectionCycleJobRequest(
  94. string JCode,
  95. string? CycleId,
  96. string? CycleName);
  97. public sealed record ElectionCycleJobResponse(
  98. string JobId,
  99. string JCode,
  100. string CycleId,
  101. string CycleName,
  102. string Status,
  103. string CreatedBy,
  104. string CreatedAt)
  105. {
  106. public static ElectionCycleJobResponse From(ElectionCycleJob job) =>
  107. new(
  108. JobId: job.JobId,
  109. JCode: job.JCode,
  110. CycleId: job.CycleId,
  111. CycleName: job.CycleName,
  112. Status: job.Status,
  113. CreatedBy: job.CreatedBy,
  114. CreatedAt: job.CreatedAt.ToString("O"));
  115. }
  116. public sealed record ElectionCycleJobProblem(string Error);

Powered by TurnKey Linux.