165 lines
6.0 KiB
C#
165 lines
6.0 KiB
C#
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)
|
|
{
|
|
if (alertType != AlertType.SofaSepsis)
|
|
throw new InvalidOperationException(
|
|
$"Sepsis bundle can only be triggered by SOFA_SEPSIS, not {alertType.ToDbString()}.");
|
|
|
|
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);
|
|
}
|
|
|
|
}
|