151 lines
6.0 KiB
C#
151 lines
6.0 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Options;
|
|
using Minio.DataModel.Args;
|
|
using RabbitMQ.Client;
|
|
using RabbitMQ.Client.Events;
|
|
|
|
public sealed class DischargeSummaryWorkerService : BackgroundService
|
|
{
|
|
private readonly IOptions<RabbitMqOptions> _rabbitOpts;
|
|
private readonly IOptions<MinioOptions> _minioOpts;
|
|
private readonly IServiceScopeFactory _scopes;
|
|
private readonly ILogger<DischargeSummaryWorkerService> _logger;
|
|
|
|
public DischargeSummaryWorkerService(
|
|
IOptions<RabbitMqOptions> rabbitOpts,
|
|
IOptions<MinioOptions> minioOpts,
|
|
IServiceScopeFactory scopes,
|
|
ILogger<DischargeSummaryWorkerService> logger)
|
|
{
|
|
_rabbitOpts = rabbitOpts;
|
|
_minioOpts = minioOpts;
|
|
_scopes = scopes;
|
|
_logger = logger;
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
|
|
|
var o = _rabbitOpts.Value;
|
|
var factory = RabbitMqConnectionFactory.Create(o, dispatchConsumersAsync: true);
|
|
|
|
using var connection = factory.CreateConnection("discharge-summary-worker");
|
|
using var channel = connection.CreateModel();
|
|
|
|
channel.BasicQos(0, prefetchCount: 3, global: false);
|
|
|
|
var consumer = new AsyncEventingBasicConsumer(channel);
|
|
consumer.Received += async (sender, ea) =>
|
|
{
|
|
await HandleDischargeSummaryAsync(channel, ea, stoppingToken);
|
|
};
|
|
|
|
channel.BasicConsume("notifications.discharge.queue", autoAck: false, consumer);
|
|
|
|
_logger.LogInformation("DischargeSummaryWorkerService consuming notifications.discharge.queue");
|
|
|
|
await Task.Delay(Timeout.Infinite, stoppingToken);
|
|
}
|
|
|
|
private async Task HandleDischargeSummaryAsync(IModel channel, BasicDeliverEventArgs ea, CancellationToken ct)
|
|
{
|
|
var payload = Encoding.UTF8.GetString(ea.Body.Span);
|
|
var doc = JsonDocument.Parse(payload);
|
|
var encounterId = Guid.Parse(doc.RootElement.GetProperty("encounterId").GetString()!);
|
|
|
|
try
|
|
{
|
|
var summary = await BuildSummaryAsync(encounterId, ct);
|
|
await UploadToMinioAsync(encounterId, summary, ct);
|
|
|
|
channel.BasicAck(ea.DeliveryTag, multiple: false);
|
|
|
|
_logger.LogInformation(
|
|
"[DISCHARGE-SUMMARY] Uploaded for encounter {EncounterId}", encounterId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "DischargeSummaryWorker failed for encounter {EncounterId}", encounterId);
|
|
channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: true);
|
|
}
|
|
}
|
|
|
|
private async Task<string> BuildSummaryAsync(Guid encounterId, CancellationToken ct)
|
|
{
|
|
await using var scope = _scopes.CreateAsyncScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
|
|
var encounter = await db.Encounters
|
|
.Include(e => e.Patient)
|
|
.FirstOrDefaultAsync(e => e.Id == encounterId, ct);
|
|
|
|
if (encounter is null)
|
|
return $"DISCHARGE SUMMARY\nEncounter {encounterId} not found in database.";
|
|
|
|
var obsCount = await db.Observations.CountAsync(o => o.EncounterId == encounterId, ct);
|
|
var alertCount = await db.ClinicalAlerts.CountAsync(a => a.EncounterId == encounterId, ct);
|
|
var orders = await db.Orders
|
|
.Where(o => o.EncounterId == encounterId)
|
|
.Select(o => new { o.OrderType, o.Description, o.Status, o.OrderedAt, o.ResultedAt })
|
|
.ToListAsync(ct);
|
|
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("=== VIGILCARE DISCHARGE SUMMARY ===");
|
|
sb.AppendLine($"Generated: {DateTimeOffset.UtcNow:u}");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"Patient: {encounter.Patient.FirstName} {encounter.Patient.LastName}");
|
|
sb.AppendLine($"MRN: {encounter.Patient.Mrn}");
|
|
sb.AppendLine($"Date of Birth: {encounter.Patient.DateOfBirth:yyyy-MM-dd}");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"Encounter ID: {encounter.Id}");
|
|
sb.AppendLine($"Type: {encounter.EncounterType}");
|
|
sb.AppendLine($"Department: {encounter.Department.ToDbString()}");
|
|
sb.AppendLine($"Attending: {encounter.AttendingPhysician}");
|
|
sb.AppendLine($"Admitted: {encounter.AdmittedAt:u}");
|
|
sb.AppendLine($"Discharged: {encounter.DischargedAt:u}");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"Observations Recorded: {obsCount}");
|
|
sb.AppendLine($"Clinical Alerts: {alertCount}");
|
|
sb.AppendLine();
|
|
|
|
if (orders.Any())
|
|
{
|
|
sb.AppendLine("--- Orders ---");
|
|
foreach (var o in orders)
|
|
sb.AppendLine($" [{o.Status.ToDbString()}] {o.OrderType.ToDbString()}: {o.Description} (ordered: {o.OrderedAt:u})");
|
|
}
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine("NOTE: This summary is generated automatically. Clinician review required.");
|
|
sb.AppendLine("NOTE: HIPAA Notice — This document contains protected health information.");
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private async Task UploadToMinioAsync(Guid encounterId, string content, CancellationToken ct)
|
|
{
|
|
var mo = _minioOpts.Value;
|
|
var client = MinioClientFactory.Build(mo);
|
|
var bucket = mo.BucketName;
|
|
|
|
var exists = await client.BucketExistsAsync(
|
|
new BucketExistsArgs().WithBucket(bucket), ct);
|
|
|
|
if (!exists)
|
|
await client.MakeBucketAsync(new MakeBucketArgs().WithBucket(bucket), ct);
|
|
|
|
var objectKey = $"discharge-summaries/{encounterId}/summary.pdf";
|
|
var bytes = Encoding.UTF8.GetBytes(content);
|
|
|
|
await client.PutObjectAsync(new PutObjectArgs()
|
|
.WithBucket(bucket)
|
|
.WithObject(objectKey)
|
|
.WithStreamData(new MemoryStream(bytes))
|
|
.WithObjectSize(bytes.Length)
|
|
.WithContentType("text/plain"),
|
|
ct);
|
|
}
|
|
} |