using Microsoft.EntityFrameworkCore; using StackExchange.Redis; public class AlertThresholdService : IAlertThresholdService { private readonly AppDbContext _db; private readonly IConnectionMultiplexer _redis; private readonly IAuditService _audit; public AlertThresholdService( AppDbContext db, IConnectionMultiplexer redis, IAuditService audit) { _db = db; _redis = redis; _audit = audit; } public async Task CreateAsync(AlertThresholdRequest req) { var exists = await _db.AlertThresholds.AnyAsync(t => t.ObservationCode == req.ObservationCode); if (exists) throw new ConflictException( "A threshold for this observation code already exists.", "THRESHOLD_CODE_CONFLICT"); var threshold = new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = req.ObservationCode, DisplayName = req.DisplayName, Unit = req.Unit, CriticalLow = req.CriticalLow, WarningLow = req.WarningLow, WarningHigh = req.WarningHigh, CriticalHigh = req.CriticalHigh, CreatedAt = DateTimeOffset.UtcNow }; _db.AlertThresholds.Add(threshold); await _db.SaveChangesAsync(); await _audit.WriteAsync( AuditAction.ThresholdCreated, "AlertThreshold", threshold.Id, newValue: new { threshold.ObservationCode, threshold.CriticalLow, threshold.WarningLow, threshold.WarningHigh, threshold.CriticalHigh }); await InvalidateCacheAsync(threshold.ObservationCode); return threshold; } public async Task> ListAsync() => await _db.AlertThresholds.OrderBy(t => t.ObservationCode).ToListAsync(); public async Task GetByIdAsync(Guid id) { var threshold = await _db.AlertThresholds.FindAsync(id); if (threshold is null) throw new NotFoundException("Threshold not found.", "THRESHOLD_NOT_FOUND"); return threshold; } public async Task UpdateAsync(Guid id, AlertThresholdRequest req) { var threshold = await _db.AlertThresholds.FindAsync(id); if (threshold is null) throw new NotFoundException("Threshold not found.", "THRESHOLD_NOT_FOUND"); var previous = new { threshold.ObservationCode, threshold.CriticalLow, threshold.WarningLow, threshold.WarningHigh, threshold.CriticalHigh, threshold.SuppressionWindowMinutes }; threshold.DisplayName = req.DisplayName; threshold.Unit = req.Unit; threshold.CriticalLow = req.CriticalLow; threshold.WarningLow = req.WarningLow; threshold.WarningHigh = req.WarningHigh; threshold.CriticalHigh = req.CriticalHigh; await _db.SaveChangesAsync(); await _audit.WriteAsync( AuditAction.ThresholdUpdated, "AlertThreshold", threshold.Id, previousValue: previous, newValue: new { threshold.ObservationCode, threshold.CriticalLow, threshold.WarningLow, threshold.WarningHigh, threshold.CriticalHigh, threshold.SuppressionWindowMinutes }); await InvalidateCacheAsync(threshold.ObservationCode); return threshold; } private async Task InvalidateCacheAsync(string observationCode) { var cache = _redis.GetDatabase(); await cache.KeyDeleteAsync($"threshold:{observationCode}"); } }