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 _rabbitOpts; private readonly IOptions _minioOpts; private readonly IServiceScopeFactory _scopes; private readonly ILogger _logger; public DischargeSummaryWorkerService( IOptions rabbitOpts, IOptions minioOpts, IServiceScopeFactory scopes, ILogger 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 = new ConnectionFactory { HostName = o.Host, Port = o.Port, UserName = o.Username, Password = o.Password, 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 BuildSummaryAsync(Guid encounterId, CancellationToken ct) { await using var scope = _scopes.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); 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); } }