using Microsoft.EntityFrameworkCore; public class CoverSheetService : ICoverSheetService { private readonly AppDbContext _db; private readonly ILogger _logger; public CoverSheetService(AppDbContext db, ILogger logger) { _db = db; _logger = logger; } public async Task> GenerateAsync( GenerateCoverSheetsRequest request, Guid actorUserId) { var count = Math.Clamp(request.Count, 1, 100); var batchType = BatchTypeExtensions.FromDbString(request.BatchType.ToUpperInvariant()); var track = string.IsNullOrEmpty(request.Track) ? BatchTrack.Backfill : BatchTrackExtensions.FromDbString(request.Track.ToUpperInvariant()); if (request.PatientId.HasValue) { var patientExists = await _db.Patients.AnyAsync(p => p.Id == request.PatientId.Value); if (!patientExists) throw new NotFoundException("Patient not found.", "PATIENT_NOT_FOUND"); } if (request.AssignToUserId.HasValue) { var user = await _db.Users.FindAsync(request.AssignToUserId.Value); if (user is null || !user.IsActive) throw new NotFoundException("User not found or inactive.", "USER_NOT_FOUND"); } var sheets = new List(count); for (var i = 0; i < count; i++) { var code = $"VCR-CS-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}"; sheets.Add(new CoverSheet { Id = Guid.NewGuid(), Code = code, BatchType = batchType, Track = track, PatientId = request.PatientId, AssignToUserId = request.AssignToUserId, GeneratedByUserId = actorUserId, IsUsed = false, CreatedAt = DateTimeOffset.UtcNow, }); } _db.CoverSheets.AddRange(sheets); await _db.SaveChangesAsync(); _logger.LogInformation( "Generated {Count} cover sheets for batch type {BatchType}, track {Track}", count, batchType.ToDbString(), track.ToDbString()); return sheets; } public async Task LookupByCodeAsync(string code) { return await _db.CoverSheets .Include(c => c.Patient) .Include(c => c.AssignToUser) .FirstOrDefaultAsync(c => c.Code == code.ToUpperInvariant().Trim()); } public async Task LookupByIdAsync(Guid id) { return await _db.CoverSheets .Include(c => c.Patient) .Include(c => c.AssignToUser) .FirstOrDefaultAsync(c => c.Id == id); } public async Task> GetByIdsAsync(IReadOnlyList ids) { if (ids.Count == 0) return new List(); var idSet = ids.Distinct().ToList(); var sheets = await _db.CoverSheets .AsNoTracking() .Include(c => c.Patient) .Include(c => c.AssignToUser) .Where(c => idSet.Contains(c.Id)) .ToListAsync(); var byId = sheets.ToDictionary(c => c.Id); return idSet .Where(byId.ContainsKey) .Select(id => byId[id]) .ToList(); } public async Task RedeemAsync(Guid coverSheetId, Guid batchId) { var sheet = await _db.CoverSheets.FindAsync(coverSheetId); if (sheet is null) throw new NotFoundException("Cover sheet not found.", "COVER_SHEET_NOT_FOUND"); if (sheet.IsUsed) throw new ConflictException( $"Cover sheet {sheet.Code} has already been used for batch {sheet.BatchId}.", "COVER_SHEET_ALREADY_USED"); sheet.IsUsed = true; sheet.BatchId = batchId; sheet.UsedAt = DateTimeOffset.UtcNow; await _db.SaveChangesAsync(); } public async Task> ListAsync( bool? isUsed, Guid? patientId, int page, int pageSize) { var query = _db.CoverSheets .AsNoTracking() .Include(c => c.Patient) .Include(c => c.AssignToUser) .AsQueryable(); if (isUsed.HasValue) query = query.Where(c => c.IsUsed == isUsed.Value); if (patientId.HasValue) query = query.Where(c => c.PatientId == patientId.Value); return await query .OrderByDescending(c => c.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); } }