feature: Approval and Promotion to VigilCareClinical
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
/// <summary>
|
||||
/// Approval and promotion of verified digitization batches to VigilCareClinical live tables.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/digitization-batches")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class ApprovalController : ControllerBase
|
||||
{
|
||||
private readonly IPromotionService _promotion;
|
||||
private readonly ILogger<ApprovalController> _logger;
|
||||
|
||||
public ApprovalController(IPromotionService promotion, ILogger<ApprovalController> logger)
|
||||
{
|
||||
_promotion = promotion;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="id">The batch ID to approve.</param>
|
||||
/// <param name="request">Optional request body with alert configuration.</param>
|
||||
/// <returns>Promotion result with live IDs for patient, encounter, and observations.</returns>
|
||||
[HttpPost("{id:guid}/approve")]
|
||||
[Authorize(Roles = "CLINICAL_APPROVER,ADMINISTRATOR")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PromotionResultResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<IActionResult> Approve(Guid id, [FromBody] ApproveRequest? request)
|
||||
{
|
||||
var idempotencyKey = Request.Headers["Idempotency-Key"].FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(idempotencyKey))
|
||||
return BadRequest(ApiResponse<object>.Fail(400,
|
||||
"Idempotency-Key header is required for promotion operations.",
|
||||
"MISSING_IDEMPOTENCY_KEY"));
|
||||
|
||||
if (idempotencyKey.Length > 100)
|
||||
return BadRequest(ApiResponse<object>.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<PromotionResultResponse>.Ok(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the promotion result for an already-promoted batch, including the live IDs
|
||||
/// for Patient, Encounter, and Observations that were created during promotion.
|
||||
/// </summary>
|
||||
/// <param name="id">The batch ID to query.</param>
|
||||
/// <returns>Promotion result with live clinical entity IDs.</returns>
|
||||
[HttpGet("{id:guid}/promotion-result")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(ApiResponse<PromotionResultResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> GetPromotionResult(Guid id)
|
||||
{
|
||||
var result = await _promotion.GetPromotionResultAsync(id);
|
||||
return Ok(ApiResponse<PromotionResultResponse>.Ok(result));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user