using Microsoft.EntityFrameworkCore; public class LocalAlertService : ILocalAlertService { private readonly GatewayDbContext _db; public LocalAlertService(GatewayDbContext db) => _db = db; public async Task AcknowledgeAsync( Guid id, AcknowledgeAlertRequest req, string clinicianId) { var alert = await _db.ClinicalAlerts.FindAsync(id) ?? throw new NotFoundException("Alert not found.", "ALERT_NOT_FOUND"); if (alert.Status is not (AlertStatus.Open or AlertStatus.Escalated)) throw new ConflictException("Alert cannot be acknowledged.", "ALERT_NOT_ACKNOWLEDGEABLE"); alert.Status = AlertStatus.Acknowledged; alert.AcknowledgedAt = DateTimeOffset.UtcNow; alert.AcknowledgedBy = clinicianId; BufferedSyncWriter.Enqueue(_db, BufferedSyncItemType.Ack, new { clientRef = Guid.NewGuid(), alert.ClientAlertId, clinicianId, acknowledgedAt = alert.AcknowledgedAt, req.Note }, $"ack:{alert.ClientAlertId}:{alert.AcknowledgedAt:o}", alert.EncounterId, alert.AcknowledgedAt!.Value); await _db.SaveChangesAsync(); return alert; } public async Task ResolveAsync(Guid id) { var alert = await _db.ClinicalAlerts.FindAsync(id) ?? throw new NotFoundException("Alert not found.", "ALERT_NOT_FOUND"); if (alert.Status != AlertStatus.Acknowledged) throw new ConflictException("Alert must be acknowledged first.", "ALERT_NOT_ACKNOWLEDGED"); alert.Status = AlertStatus.Resolved; alert.ResolvedAt = DateTimeOffset.UtcNow; BufferedSyncWriter.Enqueue(_db, BufferedSyncItemType.Resolve, new { clientRef = Guid.NewGuid(), alert.ClientAlertId, clinicianId = alert.AcknowledgedBy, resolvedAt = alert.ResolvedAt, note = (string?)null }, $"resolve:{alert.ClientAlertId}", alert.EncounterId, alert.ResolvedAt!.Value); await _db.SaveChangesAsync(); return alert; } public async Task> ListAsync( Guid? encounterId, string? status, int page, int pageSize) { page = Math.Max(1, page); pageSize = Math.Clamp(pageSize, 1, 100); var q = _db.ClinicalAlerts.AsNoTracking().AsQueryable(); if (encounterId.HasValue) q = q.Where(a => a.EncounterId == encounterId); if (!string.IsNullOrEmpty(status)) { var parsedStatus = AlertStatusExtensions.FromDbString(status); q = q.Where(a => a.Status == parsedStatus); } var total = await q.CountAsync(); var items = await q.OrderByDescending(a => a.TriggeredAt) .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); return new PagedResult(items, page, pageSize, total); } }