initial commit
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Batch CRUD, document upload, and assignment for the digitization workflow.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/digitization-batches")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class DigitizationBatchesController : ControllerBase
|
||||
{
|
||||
private readonly IBatchService _batches;
|
||||
private readonly IDocumentStorageService _storage;
|
||||
|
||||
private static readonly HashSet<string> _allowedMimeTypes = new()
|
||||
{
|
||||
"application/pdf", "image/jpeg", "image/png"
|
||||
};
|
||||
|
||||
public DigitizationBatchesController(IBatchService batches, IDocumentStorageService storage)
|
||||
{
|
||||
_batches = batches;
|
||||
_storage = storage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a scanned document and creates a new digitization batch.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[RequestSizeLimit(25 * 1024 * 1024)]
|
||||
public async Task<IActionResult> Create(
|
||||
IFormFile file,
|
||||
[FromForm] CreateBatchRequest req)
|
||||
{
|
||||
if (file is null || file.Length == 0)
|
||||
return BadRequest(ApiResponse<object>.Fail(400, "File is required.", "EMPTY_FILE"));
|
||||
|
||||
if (!_allowedMimeTypes.Contains(file.ContentType))
|
||||
return BadRequest(ApiResponse<object>.Fail(400,
|
||||
"Accepted formats: PDF, JPEG, PNG.", "INVALID_MIME_TYPE"));
|
||||
|
||||
var parsedBatchType = BatchTypeExtensions.FromDbString(req.BatchType.ToUpperInvariant());
|
||||
var parsedTrack = string.IsNullOrEmpty(req.Track)
|
||||
? BatchTrack.Backfill
|
||||
: BatchTrackExtensions.FromDbString(req.Track.ToUpperInvariant());
|
||||
var actorUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
|
||||
using var stream = file.OpenReadStream();
|
||||
var batch = await _batches.CreateAsync(
|
||||
stream, file.ContentType, parsedBatchType, parsedTrack, req.PatientId, actorUserId);
|
||||
return StatusCode(201, ApiResponse<BatchDetailResponse>.Created(BatchDetailResponse.FromEntity(batch)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a batch by ID with a presigned document URL (15-minute expiry).
|
||||
/// </summary>
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Get(Guid id)
|
||||
{
|
||||
var batch = await _batches.GetByIdAsync(id);
|
||||
var presignedUrl = await _storage.GetPresignedUrlAsync(batch.DocumentRef);
|
||||
|
||||
return Ok(ApiResponse<BatchDetailResponse>.Ok(
|
||||
BatchDetailResponse.FromEntity(batch, presignedUrl)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists batches with optional filters and pagination.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> List(
|
||||
[FromQuery] string? status,
|
||||
[FromQuery] string? batchType,
|
||||
[FromQuery] Guid? assignedTo,
|
||||
[FromQuery] string? track,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20)
|
||||
{
|
||||
BatchStatus? parsedStatus = string.IsNullOrEmpty(status) ? null : BatchStatusExtensions.FromDbString(status.ToUpperInvariant());
|
||||
BatchType? parsedBatchType = string.IsNullOrEmpty(batchType) ? null : BatchTypeExtensions.FromDbString(batchType.ToUpperInvariant());
|
||||
BatchTrack? parsedTrack = string.IsNullOrEmpty(track) ? null : BatchTrackExtensions.FromDbString(track.ToUpperInvariant());
|
||||
|
||||
var result = await _batches.ListAsync(parsedStatus, parsedBatchType, assignedTo, parsedTrack, page, pageSize);
|
||||
return Ok(ApiResponse<object>.Ok(new
|
||||
{
|
||||
items = result.Items,
|
||||
page = result.Page,
|
||||
pageSize = result.PageSize,
|
||||
totalCount = result.TotalCount,
|
||||
totalPages = result.TotalPages
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns a batch to an entry clerk. Uses Redis lock to prevent double-assignment.
|
||||
/// </summary>
|
||||
[HttpPatch("{id:guid}/assign")]
|
||||
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Assign(Guid id, [FromBody] AssignBatchRequest req)
|
||||
{
|
||||
var actorUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
var batch = await _batches.AssignAsync(id, req.EntryClerkUserId, actorUserId);
|
||||
return Ok(ApiResponse<BatchDetailResponse>.Ok(BatchDetailResponse.FromEntity(batch)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user