using Microsoft.EntityFrameworkCore; using StackExchange.Redis; public class QsofaService : IQsofaService { private readonly AppDbContext _db; private readonly IConnectionMultiplexer _redis; public QsofaService(AppDbContext db, IConnectionMultiplexer redis) { _db = db; _redis = redis; } public async Task GetCurrentAsync(Guid encounterId) { await EnsureEncounterExistsAsync(encounterId); var values = await ReadCriterionValuesAsync(encounterId); return BuildResponse(values); } public async Task GetActiveCriteriaCountAsync(Guid encounterId) { var values = await ReadCriterionValuesAsync(encounterId); return QsofaCalculator.CountActiveCriteria(values); } public async Task> GetHistoryAsync( Guid encounterId, int limit, string? cursor) { limit = Math.Clamp(limit, 1, 100); var query = _db.ClinicalAlerts .AsNoTracking() .Where(a => a.EncounterId == encounterId #pragma warning disable CS0618 // Include legacy QSOFA_WARNING rows in history && (a.AlertType == AlertType.QsofaScreen || a.AlertType == AlertType.QsofaWarning)); #pragma warning restore CS0618 var items = await query .OrderByDescending(a => a.TriggeredAt) .ThenByDescending(a => a.Id) .Take(limit + 1) .ToListAsync(); var hasMore = items.Count > limit; if (hasMore) items.RemoveAt(limit); return new CursorPage(items, null, hasMore); } private async Task EnsureEncounterExistsAsync(Guid encounterId) { var exists = await _db.Encounters.AnyAsync(e => e.Id == encounterId); if (!exists) throw new NotFoundException("Encounter not found.", "ENCOUNTER_NOT_FOUND"); } private async Task ReadCriterionValuesAsync(Guid encounterId) { var cache = _redis.GetDatabase(); return await cache.StringGetAsync(QsofaCalculator.AllCriterionKeys(encounterId)); } private static QsofaCurrentResponse BuildResponse(RedisValue[] values) { return new QsofaCurrentResponse( QsofaCalculator.CountActiveCriteria(values), new QsofaCriteriaState( ParseOptionalDecimal(values[0]), ParseOptionalDecimal(values[1]), ParseOptionalDecimal(values[2]))); } private static decimal? ParseOptionalDecimal(RedisValue value) => value.HasValue ? decimal.Parse(value.ToString()) : null; }