feature: qSOFA Scoring & Sepsis Bundle Compliance
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class SepsisBundleService : ISepsisBundleService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IOrderService _orderService;
|
||||
private readonly ClinicalMetrics _metrics;
|
||||
|
||||
public SepsisBundleService(AppDbContext db, IOrderService orderService, ClinicalMetrics metrics)
|
||||
{
|
||||
_db = db;
|
||||
_orderService = orderService;
|
||||
_metrics = metrics;
|
||||
}
|
||||
|
||||
public async Task<SepsisBundle?> TryCreateBundleAsync(
|
||||
Guid encounterId, Guid triggeringAlertId, AlertType alertType, CancellationToken ct = default)
|
||||
{
|
||||
var exists = await _db.SepsisBundles
|
||||
.AnyAsync(b => b.EncounterId == encounterId
|
||||
&& b.ComplianceStatus == SepsisBundleComplianceStatus.InProgress, ct);
|
||||
|
||||
if (exists)
|
||||
return null;
|
||||
|
||||
var recognizedAt = DateTimeOffset.UtcNow;
|
||||
var bundle = new SepsisBundle
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
EncounterId = encounterId,
|
||||
TriggeringAlertId = triggeringAlertId,
|
||||
TriggeringAlertType = alertType.ToDbString(),
|
||||
RecognizedAt = recognizedAt,
|
||||
DeadlineAt = recognizedAt.AddHours(1),
|
||||
ComplianceStatus = SepsisBundleComplianceStatus.InProgress
|
||||
};
|
||||
|
||||
var elementDefs = new (SepsisBundleElementType Type, OrderType OrderType, string Description)[]
|
||||
{
|
||||
(SepsisBundleElementType.BloodCultures, OrderType.Lab, "SEP-1: Blood cultures"),
|
||||
(SepsisBundleElementType.SerumLactate, OrderType.Lab, "SEP-1: Serum lactate"),
|
||||
(SepsisBundleElementType.BroadSpectrumAntibiotics, OrderType.Medication, "SEP-1: Broad-spectrum antibiotics"),
|
||||
(SepsisBundleElementType.IvFluidResuscitation, OrderType.Procedure, "SEP-1: IV fluid bolus"),
|
||||
};
|
||||
|
||||
foreach (var (elementType, orderType, description) in elementDefs)
|
||||
{
|
||||
var order = await _orderService.CreateAsync(encounterId, new CreateOrderRequest(
|
||||
orderType, description, "sepsis-bundle-engine"));
|
||||
|
||||
bundle.Elements.Add(new SepsisBundleElement
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ElementType = elementType,
|
||||
Status = SepsisBundleElementStatus.Pending,
|
||||
OrderId = order.Id
|
||||
});
|
||||
}
|
||||
|
||||
_db.SepsisBundles.Add(bundle);
|
||||
|
||||
_db.OutboxEvents.Add(new OutboxEvent
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Topic = "sepsis.bundle.created",
|
||||
Payload = JsonSerializer.Serialize(new
|
||||
{
|
||||
bundleId = bundle.Id,
|
||||
encounterId,
|
||||
triggeringAlertId,
|
||||
triggeringAlertType = alertType.ToDbString(),
|
||||
recognizedAt,
|
||||
deadlineAt = bundle.DeadlineAt,
|
||||
partitionKey = encounterId.ToString()
|
||||
}),
|
||||
PartitionKey = encounterId.ToString(),
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
public async Task<SepsisBundle?> GetCurrentByEncounterAsync(Guid encounterId)
|
||||
{
|
||||
return await _db.SepsisBundles
|
||||
.AsNoTracking()
|
||||
.Include(b => b.Elements)
|
||||
.ThenInclude(e => e.Order)
|
||||
.Where(b => b.EncounterId == encounterId)
|
||||
.OrderByDescending(b => b.RecognizedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<SepsisBundle> GetByIdAsync(Guid id)
|
||||
{
|
||||
var bundle = await _db.SepsisBundles
|
||||
.AsNoTracking()
|
||||
.Include(b => b.Elements)
|
||||
.ThenInclude(e => e.Order)
|
||||
.FirstOrDefaultAsync(b => b.Id == id);
|
||||
|
||||
if (bundle is null)
|
||||
throw new NotFoundException("Sepsis bundle not found.", "BUNDLE_NOT_FOUND");
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
public async Task OnOrderResultedAsync(Guid orderId, CancellationToken ct = default)
|
||||
{
|
||||
var element = await _db.SepsisBundleElements
|
||||
.Include(e => e.Bundle)
|
||||
.FirstOrDefaultAsync(e => e.OrderId == orderId, ct);
|
||||
|
||||
if (element is null || element.Status == SepsisBundleElementStatus.Completed)
|
||||
return;
|
||||
|
||||
element.Status = SepsisBundleElementStatus.Completed;
|
||||
element.CompletedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
var bundle = element.Bundle;
|
||||
var allComplete = await _db.SepsisBundleElements
|
||||
.Where(e => e.BundleId == bundle.Id && e.Id != element.Id)
|
||||
.AllAsync(e => e.Status == SepsisBundleElementStatus.Completed, ct);
|
||||
|
||||
if (allComplete)
|
||||
{
|
||||
bundle.CompletedAt = DateTimeOffset.UtcNow;
|
||||
bundle.ComplianceStatus = bundle.CompletedAt <= bundle.DeadlineAt
|
||||
? SepsisBundleComplianceStatus.Compliant
|
||||
: SepsisBundleComplianceStatus.NonCompliant;
|
||||
|
||||
_metrics.SepsisBundleComplianceTotal
|
||||
.WithLabels(bundle.ComplianceStatus.ToDbString()).Inc();
|
||||
}
|
||||
|
||||
_db.OutboxEvents.Add(new OutboxEvent
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Topic = "sepsis.bundle.updated",
|
||||
Payload = JsonSerializer.Serialize(new
|
||||
{
|
||||
bundleId = bundle.Id,
|
||||
encounterId = bundle.EncounterId,
|
||||
elementId = element.Id,
|
||||
elementType = element.ElementType.ToDbString(),
|
||||
elementStatus = element.Status.ToDbString(),
|
||||
complianceStatus = bundle.ComplianceStatus.ToDbString(),
|
||||
completedAt = element.CompletedAt,
|
||||
partitionKey = bundle.EncounterId.ToString()
|
||||
}),
|
||||
PartitionKey = bundle.EncounterId.ToString(),
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user