using System.Text.Json; using Microsoft.EntityFrameworkCore; public class AlertService : IAlertService { private readonly AppDbContext _db; public AlertService(AppDbContext db) => _db = db; public async Task> 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(alerts, page, pageSize, total); } public async Task> ListGlobalAsync( AlertStatus? status, AlertSeverity? severity, Department? 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 (department.HasValue) query = query.Where(a => a.Encounter.Department == department.Value); var total = await query.CountAsync(); var alerts = await query .OrderByDescending(a => a.TriggeredAt) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return new PagedResult(alerts, page, pageSize, total); } public async Task 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 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 }), PartitionKey = alert.EncounterId.ToString(), CreatedAt = DateTimeOffset.UtcNow }); await _db.SaveChangesAsync(); return alert; } public async Task 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; } }