feature: Observation Ingest, Synchronous Alert Detection, and Alert Lifecycle
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class AlertService : IAlertService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public AlertService(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<PagedResult<ClinicalAlert>> ListByEncounterAsync(
|
||||
Guid encounterId, AlertStatus? status, int page, int pageSize)
|
||||
{
|
||||
var query = _db.ClinicalAlerts
|
||||
.AsNoTracking()
|
||||
.Where(a => a.EncounterId == encounterId);
|
||||
|
||||
if (status.HasValue)
|
||||
query = query.Where(a => a.Status == status.Value);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var alerts = await query
|
||||
.OrderByDescending(a => a.TriggeredAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
return new PagedResult<ClinicalAlert>(alerts, page, pageSize, total);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<ClinicalAlert>> ListGlobalAsync(
|
||||
AlertStatus? status, AlertSeverity? severity, string? department, int page, int pageSize)
|
||||
{
|
||||
var query = _db.ClinicalAlerts
|
||||
.AsNoTracking()
|
||||
.Include(a => a.Encounter)
|
||||
.AsQueryable();
|
||||
|
||||
if (status.HasValue)
|
||||
query = query.Where(a => a.Status == status.Value);
|
||||
|
||||
if (severity.HasValue)
|
||||
query = query.Where(a => a.Severity == severity.Value);
|
||||
|
||||
if (!string.IsNullOrEmpty(department))
|
||||
query = query.Where(a => a.Encounter.Department == department);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var alerts = await query
|
||||
.OrderByDescending(a => a.TriggeredAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
return new PagedResult<ClinicalAlert>(alerts, page, pageSize, total);
|
||||
}
|
||||
|
||||
public async Task<ClinicalAlert> GetByIdAsync(Guid id)
|
||||
{
|
||||
var alert = await _db.ClinicalAlerts
|
||||
.AsNoTracking()
|
||||
.Include(a => a.Encounter)
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
if (alert is null)
|
||||
throw new NotFoundException("Alert not found.", "ALERT_NOT_FOUND");
|
||||
|
||||
return alert;
|
||||
}
|
||||
|
||||
public async Task<ClinicalAlert> AcknowledgeAsync(Guid id, AcknowledgeAlertRequest req)
|
||||
{
|
||||
var alert = await _db.ClinicalAlerts.FindAsync(id);
|
||||
if (alert is null)
|
||||
throw new NotFoundException("Alert not found.", "ALERT_NOT_FOUND");
|
||||
|
||||
if (alert.Status != AlertStatus.Open && alert.Status != AlertStatus.Escalated)
|
||||
throw new ConflictException(
|
||||
$"Alert cannot be acknowledged from status '{alert.Status}'.",
|
||||
"ALERT_NOT_ACKNOWLEDGEABLE");
|
||||
|
||||
alert.Status = AlertStatus.Acknowledged;
|
||||
alert.AcknowledgedAt = DateTimeOffset.UtcNow;
|
||||
alert.AcknowledgedBy = req.ClinicianId;
|
||||
|
||||
// Write an outbox event so the Kafka consumer (Phase 6) can cancel the
|
||||
// pending RabbitMQ escalation timer when it sees this acknowledgment.
|
||||
_db.OutboxEvents.Add(new OutboxEvent
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Topic = "alert.acknowledged",
|
||||
Payload = JsonSerializer.Serialize(new
|
||||
{
|
||||
alertId = alert.Id,
|
||||
encounterId = alert.EncounterId,
|
||||
acknowledgedBy = req.ClinicianId,
|
||||
acknowledgedAt = alert.AcknowledgedAt,
|
||||
note = req.Note
|
||||
}),
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return alert;
|
||||
}
|
||||
|
||||
public async Task<ClinicalAlert> ResolveAsync(Guid id)
|
||||
{
|
||||
var alert = await _db.ClinicalAlerts.FindAsync(id);
|
||||
if (alert is null)
|
||||
throw new NotFoundException("Alert not found.", "ALERT_NOT_FOUND");
|
||||
|
||||
if (alert.Status != AlertStatus.Acknowledged)
|
||||
throw new ConflictException(
|
||||
"Alert must be acknowledged before it can be resolved.",
|
||||
"ALERT_NOT_ACKNOWLEDGED");
|
||||
|
||||
alert.Status = AlertStatus.Resolved;
|
||||
alert.ResolvedAt = DateTimeOffset.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return alert;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user