using Microsoft.EntityFrameworkCore; public class DigitizationHistoryService : IDigitizationHistoryService { private readonly AppDbContext _db; public DigitizationHistoryService(AppDbContext db) { _db = db; } public async Task GetPatientHistoryAsync(Guid patientId) { // Verify the patient exists (has at least one batch) var hasBatches = await _db.DigitizationBatches .AnyAsync(b => b.PatientId == patientId); if (!hasBatches) throw new NotFoundException( $"No digitization history found for patient {patientId}.", "PATIENT_HISTORY_NOT_FOUND"); // Load all batches for this patient with their relationships var batches = await _db.DigitizationBatches .Include(b => b.DraftObservations) .Include(b => b.Events) .ThenInclude(e => e.Actor) .Include(b => b.EnteredByUser) .Include(b => b.VerifiedByUser) .Include(b => b.ApprovedByUser) .Where(b => b.PatientId == patientId) .OrderByDescending(b => b.CreatedAt) .ToListAsync(); // For each promoted batch, count live vs superseded observations var promotedBatchIds = batches .Where(b => b.Status == BatchStatus.Promoted) .Select(b => b.Id) .ToList(); var liveObservationCounts = await _db.LiveObservations .Where(o => promotedBatchIds.Contains(o.SourceBatchId)) .GroupBy(o => new { o.SourceBatchId, o.IsSuperseded }) .Select(g => new { g.Key.SourceBatchId, g.Key.IsSuperseded, Count = g.Count() }) .ToListAsync(); // Build a lookup: batchId -> (activeCount, supersededCount) var obsCountLookup = promotedBatchIds.ToDictionary( id => id, id => { var active = liveObservationCounts .FirstOrDefault(x => x.SourceBatchId == id && !x.IsSuperseded)?.Count ?? 0; var superseded = liveObservationCounts .FirstOrDefault(x => x.SourceBatchId == id && x.IsSuperseded)?.Count ?? 0; return (Active: active, Superseded: superseded); }); // Build a lookup for "has been superseded" — batches that appear as // SupersedesBatchId on another batch that reached Promoted var supersededByLookup = await _db.DigitizationBatches .Where(b => b.SupersedesBatchId.HasValue && b.Status == BatchStatus.Promoted && promotedBatchIds.Contains(b.SupersedesBatchId.Value)) .ToDictionaryAsync( b => b.SupersedesBatchId!.Value, b => b.Id); var entries = batches.Select(b => { var counts = obsCountLookup.GetValueOrDefault(b.Id, (Active: 0, Superseded: 0)); var hasBeenSuperseded = supersededByLookup.ContainsKey(b.Id); supersededByLookup.TryGetValue(b.Id, out var supersededByBatchId); return new DigitizationHistoryEntry( BatchId: b.Id, Status: b.Status.ToDbString(), BatchType: b.BatchType.ToDbString(), Track: b.Track.ToDbString(), SupersedesBatchId: b.SupersedesBatchId, IsCorrection: b.SupersedesBatchId.HasValue, HasBeenSuperseded: hasBeenSuperseded, SupersededByBatchId: hasBeenSuperseded ? supersededByBatchId : null, DraftObservationCount: b.DraftObservations.Count, LiveObservationCount: counts.Active, SupersededObservationCount: counts.Superseded, CreatedAt: b.CreatedAt, PromotedAt: b.PromotedAt, PromotionEncounterId: b.PromotionEncounterId, EnteredByUserId: b.EnteredByUserId, EnteredByUserName: b.EnteredByUser?.FullName, VerifiedByUserId: b.VerifiedByUserId, VerifiedByUserName: b.VerifiedByUser?.FullName, ApprovedByUserId: b.ApprovedByUserId, ApprovedByUserName: b.ApprovedByUser?.FullName, AuditTrail: b.Events .OrderBy(e => e.OccurredAt) .Select(e => new DigitizationEventSummary( e.EventType.ToDbString(), e.OccurredAt, e.ActorUserId, e.Actor?.FullName ?? "Unknown", e.MetadataJson)) .ToList() ); }).ToList(); return new PatientDigitizationHistoryResponse( PatientId: patientId, TotalBatches: entries.Count, PromotedBatches: entries.Count(e => e.Status == "PROMOTED"), SupersededBatches: entries.Count(e => e.HasBeenSuperseded), PendingBatches: entries.Count(e => e.Status != "PROMOTED" && e.Status != "REJECTED"), Entries: entries ); } }