using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; /// /// Observation ingest and cursor-paginated history for an encounter. /// [ApiController] [Route("api/v1/encounters/{encounterId:guid}/observations")] [Produces("application/json")] [Authorize] public class ObservationsController : ControllerBase { private readonly IObservationService _ingest; private readonly IObservationQueryService _query; public ObservationsController(IObservationService ingest, IObservationQueryService query) { _ingest = ingest; _query = query; } /// /// Ingests one to ten observations for an encounter in a single request. /// /// Encounter id. /// Batch of observations to record. /// Per-observation ingest results, including any generated alerts. [HttpPost] [AuthorizePermission(ClinicalPermissions.ObservationsIngest)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status422UnprocessableEntity)] public async Task Ingest(Guid encounterId, [FromBody] BatchIngestRequest req) { if (req.Observations.Count == 0) return BadRequest(ApiResponse.Fail(400, "At least one observation is required.", "EMPTY_BATCH")); if (req.Observations.Count > 10) return BadRequest(ApiResponse.Fail(400, "Batch size cannot exceed 10 observations.", "BATCH_TOO_LARGE")); var results = new List(); foreach (var obs in req.Observations) { var result = await _ingest.IngestAsync(encounterId, obs); results.Add(new { observation = result.Observation, alertGenerated = result.AlertCreated is not null, alertId = result.AlertCreated?.Id, duplicate = result.IsDuplicate }); } return StatusCode(201, ApiResponse.Created( req.Observations.Count == 1 ? (object)results[0] : results)); } /// /// Returns cursor-paginated observation history for an encounter. /// /// Encounter id. /// Optional observation code filter. /// Optional start of recorded-at range. /// Optional end of recorded-at range. /// Maximum items per page. /// Opaque cursor from a previous page. /// A page of observations with an optional next cursor. [HttpGet] [AuthorizePermission(ClinicalPermissions.EncountersRead)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task History( Guid encounterId, [FromQuery] string? code, [FromQuery] DateTimeOffset? from, [FromQuery] DateTimeOffset? to, [FromQuery] int limit = 20, [FromQuery] string? cursor = null) { var page = await _query.GetHistoryAsync(encounterId, code, from, to, limit, cursor); return Ok(ApiResponse.Ok(new { items = page.Items, nextCursor = page.NextCursor, hasMore = page.HasMore })); } }