using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Minio; using Minio.DataModel.Args; using Minio.Exceptions; public sealed class DischargeSummaryService : IDischargeSummaryService { private readonly AppDbContext _db; private readonly MinioOptions _minioOpts; public DischargeSummaryService(AppDbContext db, IOptions minioOpts) { _db = db; _minioOpts = minioOpts.Value; } public async Task GetInfoAsync(Guid encounterId, CancellationToken ct = default) { var encounter = await _db.Encounters .AsNoTracking() .FirstOrDefaultAsync(e => e.Id == encounterId, ct); if (encounter is null) throw new NotFoundException("Encounter not found."); if (encounter.Status != EncounterStatus.Discharged) { return new DischargeSummaryInfo( "NotDischarged", encounter.DischargedAt, null, null); } var exists = await ObjectExistsAsync(encounterId, ct); if (!exists) { return new DischargeSummaryInfo( "Pending", encounter.DischargedAt, null, null); } return new DischargeSummaryInfo( "Ready", encounter.DischargedAt, "text/plain", "discharge-summary.pdf"); } public async Task GetContentAsync(Guid encounterId, CancellationToken ct = default) { var info = await GetInfoAsync(encounterId, ct); if (info.Status == "NotDischarged") { throw new ConflictException( "Discharge summary is only available for discharged encounters.", "ENCOUNTER_NOT_DISCHARGED"); } if (info.Status == "Pending") { throw new NotFoundException( "Discharge summary is still being generated.", "DISCHARGE_SUMMARY_PENDING"); } var client = MinioClientFactory.Build(_minioOpts); var bucket = _minioOpts.BucketName; var objectKey = ObjectKey(encounterId); var ms = new MemoryStream(); await client.GetObjectAsync(new GetObjectArgs() .WithBucket(bucket) .WithObject(objectKey) .WithCallbackStream(stream => stream.CopyTo(ms)), ct); ms.Position = 0; return new DischargeSummaryContent(ms, info.ContentType!, info.FileName!); } private async Task ObjectExistsAsync(Guid encounterId, CancellationToken ct) { var client = MinioClientFactory.Build(_minioOpts); var bucket = _minioOpts.BucketName; var objectKey = ObjectKey(encounterId); try { await client.StatObjectAsync(new StatObjectArgs() .WithBucket(bucket) .WithObject(objectKey), ct); return true; } catch (ObjectNotFoundException) { return false; } } private static string ObjectKey(Guid encounterId) => $"discharge-summaries/{encounterId}/summary.pdf"; }