feature: Approval and Promotion to VigilCareClinical

This commit is contained in:
voltsrage
2026-06-26 14:04:52 +08:00
parent 7121520926
commit 470df683dd
21 changed files with 2638 additions and 4 deletions
@@ -0,0 +1,70 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// Batch verification and rejection endpoints.
/// Enforces separation of duties: the user who entered the data cannot verify
/// or reject the same batch. All transitions write DigitizationEvents.
/// </summary>
[ApiController]
[Route("api/v1/digitization-batches")]
[Produces("application/json")]
[Authorize]
public class VerificationController : ControllerBase
{
private readonly IVerificationService _verification;
public VerificationController(IVerificationService verification)
{
_verification = verification;
}
/// <summary>
/// Verifies a batch that is in PendingVerification status.
/// Requires field-level checks. If passed is true, transitions to Verified
/// or AwaitingClinicalApproval based on site configuration for the batch type.
/// If passed is false, transitions to Rejected with field check errors as the reason.
/// Returns 409 SEPARATION_OF_DUTIES_VIOLATION if the verifier is the same user
/// who entered the data.
/// </summary>
/// <param name="id">The batch ID to verify.</param>
/// <param name="request">Verification request with field checks and pass/fail.</param>
[HttpPost("{id:guid}/verify")]
[Authorize(Roles = "VERIFIER,CLINICAL_APPROVER,ADMINISTRATOR")]
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Verify(Guid id, [FromBody] VerifyBatchRequest request)
{
var verifierUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
var batch = await _verification.VerifyAsync(id, request, verifierUserId);
return Ok(ApiResponse<BatchDetailResponse>.Ok(BatchDetailResponse.FromEntity(batch)));
}
/// <summary>
/// Rejects a batch that is in PendingVerification or AwaitingClinicalApproval status.
/// Requires a rejection reason (minimum 10 characters). The batch returns to the
/// entry work queue for re-entry by a data entry clerk.
/// Returns 409 SEPARATION_OF_DUTIES_VIOLATION if the rejector is the same user
/// who entered the data (for PendingVerification status only).
/// </summary>
/// <param name="id">The batch ID to reject.</param>
/// <param name="request">Rejection request with required reason.</param>
[HttpPost("{id:guid}/reject")]
[Authorize(Roles = "VERIFIER,CLINICAL_APPROVER,ADMINISTRATOR")]
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Reject(Guid id, [FromBody] RejectBatchRequest request)
{
var actorUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
var batch = await _verification.RejectAsync(id, request, actorUserId);
return Ok(ApiResponse<BatchDetailResponse>.Ok(BatchDetailResponse.FromEntity(batch)));
}
}
@@ -0,0 +1,71 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// Work queue endpoints for each workflow stage.
/// Each queue returns batches filtered by status and sorted by submission time ASC
/// so the oldest pending item is always at the top.
/// </summary>
[ApiController]
[Route("api/v1/work-queue")]
[Produces("application/json")]
[Authorize]
public class WorkQueueController : ControllerBase
{
private readonly IWorkQueueService _workQueue;
public WorkQueueController(IWorkQueueService workQueue)
{
_workQueue = workQueue;
}
/// <summary>
/// Returns batches in PendingVerification status, sorted by submittedAt ASC.
/// Verifiers use this queue to pick the next batch to verify.
/// </summary>
[HttpGet("verification")]
[Authorize(Roles = "VERIFIER,CLINICAL_APPROVER,ADMINISTRATOR")]
[ProducesResponseType(typeof(ApiResponse<WorkQueueResponse>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetVerificationQueue(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var result = await _workQueue.GetVerificationQueueAsync(page, pageSize);
return Ok(ApiResponse<WorkQueueResponse>.Ok(result));
}
/// <summary>
/// Returns batches awaiting or currently in data entry.
/// Includes batches in Uploaded (awaiting assignment), InEntry (being entered),
/// and Rejected (returned for re-entry) statuses.
/// Data entry clerks use this queue to find their next assignment.
/// </summary>
[HttpGet("entry")]
[Authorize(Roles = "DATA_ENTRY_CLERK,ADMINISTRATOR")]
[ProducesResponseType(typeof(ApiResponse<WorkQueueResponse>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetEntryQueue(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var result = await _workQueue.GetEntryQueueAsync(page, pageSize);
return Ok(ApiResponse<WorkQueueResponse>.Ok(result));
}
/// <summary>
/// Returns batches in AwaitingClinicalApproval status, sorted by submittedAt ASC.
/// Clinical approvers use this queue to find batches that need clinical sign-off
/// after verification. Only batch types configured to require clinical approval
/// in site config will appear here.
/// </summary>
[HttpGet("clinical-approval")]
[Authorize(Roles = "CLINICAL_APPROVER,ADMINISTRATOR")]
[ProducesResponseType(typeof(ApiResponse<WorkQueueResponse>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetClinicalApprovalQueue(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var result = await _workQueue.GetClinicalApprovalQueueAsync(page, pageSize);
return Ok(ApiResponse<WorkQueueResponse>.Ok(result));
}
}