using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; /// /// Approval and promotion of verified digitization batches to VigilCareClinical live tables. /// [ApiController] [Route("api/v1/digitization-batches")] [Produces("application/json")] [Authorize] public class ApprovalController : ControllerBase { private readonly IPromotionService _promotion; private readonly ILogger _logger; public ApprovalController(IPromotionService promotion, ILogger logger) { _promotion = promotion; _logger = logger; } /// /// Approves a verified or awaiting-clinical-approval batch and promotes its draft data /// into live VigilCareClinical tables (Patient, Encounter, Observations). /// /// Requires the Idempotency-Key header for safe retries. If the same key is resubmitted, /// the original response is returned without re-executing the promotion. /// /// Separation of duties: the approver cannot be the entry clerk or verifier of the same batch. /// /// The batch ID to approve. /// Optional request body with alert configuration. /// Promotion result with live IDs for patient, encounter, and observations. [HttpPost("{id:guid}/approve")] [Authorize(Roles = "CLINICAL_APPROVER,ADMINISTRATOR")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status422UnprocessableEntity)] public async Task Approve(Guid id, [FromBody] ApproveRequest? request) { var idempotencyKey = Request.Headers["Idempotency-Key"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(idempotencyKey)) return BadRequest(ApiResponse.Fail(400, "Idempotency-Key header is required for promotion operations.", "MISSING_IDEMPOTENCY_KEY")); if (idempotencyKey.Length > 100) return BadRequest(ApiResponse.Fail(400, "Idempotency-Key must be at most 100 characters.", "INVALID_IDEMPOTENCY_KEY")); var approverUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); var enableRetroactiveAlerts = request?.EnableRetroactiveAlerts ?? false; _logger.LogInformation( "Approve request for batch {BatchId} by user {UserId} with idempotency key {Key}", id, approverUserId, idempotencyKey); var result = await _promotion.ApproveAndPromoteAsync( id, approverUserId, enableRetroactiveAlerts, idempotencyKey); return Ok(ApiResponse.Ok(result)); } /// /// Returns the promotion result for an already-promoted batch, including the live IDs /// for Patient, Encounter, and Observations that were created during promotion. /// /// The batch ID to query. /// Promotion result with live clinical entity IDs. [HttpGet("{id:guid}/promotion-result")] [Authorize] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] public async Task GetPromotionResult(Guid id) { var result = await _promotion.GetPromotionResultAsync(id); return Ok(ApiResponse.Ok(result)); } }