using Data.SeaHavenIndustries; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Interfaces; namespace SeaHaven.DataServices.Implementation { public class UpliftDataService : IUpliftDataService { private readonly ApplicationDbContext _context; public UpliftDataService(ApplicationDbContext context) { _context = context; } public async Task<(int TotalCount, IReadOnlyList Items)> GetPagedAsync( string? status, int? tier, int page, int pageSize, CancellationToken cancellationToken) { var query = from u in _context.DispatchUpliftRequests join d in _context.Dispatches on u.DispatchId equals d.Id join v in _context.Vendors on d.VendorId equals v.Id into vendors from v in vendors.DefaultIfEmpty() join ev in _context.VendorCompletionDocuments on u.EvidenceDocumentId equals ev.Id into evidences from ev in evidences.DefaultIfEmpty() where (u.IsDeleted == null || u.IsDeleted == false) select new { u, d, v, ev }; if (!string.IsNullOrWhiteSpace(status)) query = query.Where(x => x.u.Status == status); if (tier.HasValue) query = query.Where(x => x.u.RequiredTier == tier.Value); var total = await query.CountAsync(cancellationToken); var items = await query .OrderByDescending(x => x.u.CreatedDate) .Skip((Math.Max(page, 1) - 1) * pageSize) .Take(pageSize) .Select(x => new UpliftListItemData { Id = x.u.Id, DispatchId = x.d.Id, DispatchNumber = x.d.DispatchNumber, PONumber = x.d.PONumber, WorkOrderId = x.d.WorkOrderId, VendorCompanyName = x.v != null ? x.v.CompanyName : x.u.RequestedByVendorName, CurrentNTE = x.u.CurrentNTE, RequestedNTE = x.u.RequestedNTE, VendorReason = x.u.VendorReason, RequiredTier = x.u.RequiredTier, Status = x.u.Status, CreatedDate = x.u.CreatedDate, DecidedAt = x.u.DecidedAt, DecisionNote = x.u.DecisionNote, EvidenceDocumentId = x.u.EvidenceDocumentId, EvidenceFileName = x.ev != null ? x.ev.OriginalFileName : null, EvidenceContentType = x.ev != null ? x.ev.ContentType : null, EvidenceSizeBytes = x.ev != null ? x.ev.SizeBytes : null, ExpiresAt = x.u.ExpiresAt, NotificationStatus = x.u.NotificationStatus, NotificationError = x.u.NotificationError }) .ToListAsync(cancellationToken); return (total, items); } public async Task> GetForDispatchAsync(int dispatchId, CancellationToken cancellationToken) { return await (from u in _context.DispatchUpliftRequests where u.DispatchId == dispatchId && (u.IsDeleted == null || u.IsDeleted == false) join dec in _context.Users on u.DecidedByUserId equals dec.Id into decs from dec in decs.DefaultIfEmpty() join ev in _context.VendorCompletionDocuments on u.EvidenceDocumentId equals ev.Id into evidences from ev in evidences.DefaultIfEmpty() orderby u.CreatedDate descending select new UpliftForDispatchData { Id = u.Id, DispatchId = u.DispatchId, CurrentNTE = u.CurrentNTE, RequestedNTE = u.RequestedNTE, VendorReason = u.VendorReason, Status = u.Status, RequiredTier = u.RequiredTier, RequestedByVendorName = u.RequestedByVendorName, CreatedDate = u.CreatedDate, DecidedAt = u.DecidedAt, DecisionNote = u.DecisionNote, DecidedByFirstName = dec != null ? dec.FirstName : null, DecidedByLastName = dec != null ? dec.LastName : null, EvidenceDocumentId = u.EvidenceDocumentId, ExpiresAt = u.ExpiresAt, NotificationStatus = u.NotificationStatus, NotificationError = u.NotificationError, EvidenceFileName = ev != null ? ev.OriginalFileName : null, EvidenceContentType = ev != null ? ev.ContentType : null, EvidenceSizeBytes = ev != null ? ev.SizeBytes : null, EvidenceScanPassed = ev != null && ev.ScanStatus == "Passed" }).ToListAsync(cancellationToken); } public async Task> GetForVendorDispatchAsync(int dispatchId, CancellationToken cancellationToken) { return await (from u in _context.DispatchUpliftRequests where u.DispatchId == dispatchId && (u.IsDeleted == null || u.IsDeleted == false) join dec in _context.Users on u.DecidedByUserId equals dec.Id into decs from dec in decs.DefaultIfEmpty() join ev in _context.VendorCompletionDocuments on u.EvidenceDocumentId equals ev.Id into evidences from ev in evidences.DefaultIfEmpty() orderby u.CreatedDate descending select new PortalUpliftData { Id = u.Id, CurrentNTE = u.CurrentNTE, RequestedNTE = u.RequestedNTE, VendorReason = u.VendorReason, Status = u.Status, RequiredTier = u.RequiredTier, RequestedByVendorName = u.RequestedByVendorName, CreatedDate = u.CreatedDate, DecidedAt = u.DecidedAt, DecisionNote = u.DecisionNote, DecidedByFirstName = dec != null ? dec.FirstName : null, DecidedByLastName = dec != null ? dec.LastName : null, EvidenceDocumentId = u.EvidenceDocumentId, ExpiresAt = u.ExpiresAt, NotificationStatus = u.NotificationStatus, NotificationError = u.NotificationError, EvidenceFileName = ev != null ? ev.OriginalFileName : null, EvidenceContentType = ev != null ? ev.ContentType : null, EvidenceSizeBytes = ev != null ? ev.SizeBytes : null, EvidenceScanPassed = ev != null && ev.ScanStatus == "Passed" }).ToListAsync(cancellationToken); } public async Task GetByIdAsync(int id, CancellationToken cancellationToken) { return await _context.DispatchUpliftRequests .FirstOrDefaultAsync(u => u.Id == id, cancellationToken); } public async Task GetByIdAndDispatchAsync(int requestId, int dispatchId, CancellationToken cancellationToken) { return await _context.DispatchUpliftRequests .FirstOrDefaultAsync(u => u.Id == requestId && u.DispatchId == dispatchId, cancellationToken); } // SH-101: internal evidence download. The inner join on EvidenceDocumentId together // with the DispatchId equality filter enforces server-side request/document linkage: // a row is returned only when the document is the one linked to this exact request // and dispatch. Soft-deleted requests/documents never resolve. public async Task GetEvidenceForInternalDownloadAsync(int upliftRequestId, CancellationToken cancellationToken) { return await (from u in _context.DispatchUpliftRequests where u.Id == upliftRequestId && (u.IsDeleted == null || u.IsDeleted == false) join ev in _context.VendorCompletionDocuments on u.EvidenceDocumentId equals ev.Id where ev.DispatchId == u.DispatchId && (ev.IsDeleted == null || ev.IsDeleted == false) select new UpliftEvidenceDownloadData { Id = u.Id, DispatchId = u.DispatchId, VendorId = ev.VendorId, RequiredTier = u.RequiredTier, EvidenceDocumentId = u.EvidenceDocumentId, StoredFileName = ev.StoredFileName, OriginalFileName = ev.OriginalFileName, ContentType = ev.ContentType, Purpose = ev.Purpose, ScanStatus = ev.ScanStatus }).FirstOrDefaultAsync(cancellationToken); } public async Task HasPendingAsync(int dispatchId, CancellationToken cancellationToken) { return await _context.DispatchUpliftRequests .AnyAsync(u => u.DispatchId == dispatchId && u.Status == "Pending" && (u.IsDeleted == null || u.IsDeleted == false), cancellationToken); } public async Task HasActiveAsync(int dispatchId, CancellationToken cancellationToken) { return await _context.DispatchUpliftRequests .AnyAsync(u => u.DispatchId == dispatchId && (u.Status == "Pending" || u.Status == "ChangesRequested") && (u.IsDeleted == null || u.IsDeleted == false), cancellationToken); } public async Task GetActiveRequestAsync(int dispatchId, CancellationToken cancellationToken) { return await _context.DispatchUpliftRequests .FirstOrDefaultAsync(u => u.DispatchId == dispatchId && (u.Status == "Pending" || u.Status == "ChangesRequested") && (u.IsDeleted == null || u.IsDeleted == false), cancellationToken); } public async Task GetByRequestKeyAsync(int dispatchId, string requestKey, CancellationToken cancellationToken) { return await _context.DispatchUpliftRequests .FirstOrDefaultAsync(u => u.DispatchId == dispatchId && u.RequestKey == requestKey && (u.IsDeleted == null || u.IsDeleted == false), cancellationToken); } public async Task> GetDueForExpiryAsync(DateTime utcNow, int count, CancellationToken cancellationToken) { return await _context.DispatchUpliftRequests .Where(u => (u.Status == "Pending" || u.Status == "ChangesRequested") && u.ExpiresAt != null && u.ExpiresAt <= utcNow && (u.IsDeleted == null || u.IsDeleted == false)) .OrderBy(u => u.ExpiresAt) .Take(count) .ToListAsync(cancellationToken); } public async Task> GetDueForInitialNotificationAsync(int count, CancellationToken cancellationToken) { return await _context.DispatchUpliftRequests .Where(u => (u.Status == "Pending" || u.Status == "ChangesRequested") && u.InitialNotificationSentAt == null && (u.IsDeleted == null || u.IsDeleted == false)) .OrderBy(u => u.CreatedDate) .Take(count) .ToListAsync(cancellationToken); } public async Task> GetDueForEscalationAsync(int count, CancellationToken cancellationToken) { return await _context.DispatchUpliftRequests .Where(u => (u.Status == "Pending" || u.Status == "ChangesRequested") && u.InitialNotificationSentAt != null && u.EscalatedAt == null && (u.IsDeleted == null || u.IsDeleted == false)) .OrderBy(u => u.InitialNotificationSentAt) .Take(count) .ToListAsync(cancellationToken); } public async Task StageAsync(DispatchUpliftRequest request, CancellationToken cancellationToken) { await _context.DispatchUpliftRequests.AddAsync(request, cancellationToken); } public async Task SaveChangesAsync(CancellationToken cancellationToken) { await _context.SaveChangesAsync(cancellationToken); } } }