using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Infrastructure.Exams; public sealed class MakeupExamEligibilityService(AppDbContext db) { public async Task> GetEligibleStudentsAsync( Guid teachingTaskId, CancellationToken cancellationToken) { var gradeSheet = await db.GradeSheets.AsNoTracking() .Where(x => x.TeachingTaskId == teachingTaskId) .Select(x => new { x.Id, x.TeachingTask!.AcademicTermId }) .FirstOrDefaultAsync(cancellationToken); if (gradeSheet is null) return []; // Find students already enrolled in any makeup session for this teaching task var alreadyEnrolledIds = await db.MakeupExamEnrollments.AsNoTracking() .Where(x => x.MakeupExamSession!.TeachingTaskId == teachingTaskId && x.MakeupExamSession!.MakeupExamPlan!.Status != MakeupExamPlanStatus.Archived) .Select(x => x.StudentId) .Distinct() .ToListAsync(cancellationToken); var enrolledSet = alreadyEnrolledIds.ToHashSet(); var results = new Dictionary(); // 1. Approved deferred exams (highest priority) var deferredStudents = await db.DeferredExams.AsNoTracking() .Where(x => x.TeachingTaskId == teachingTaskId && x.Status == ApprovalStatus.Approved) .WhereNotIn(enrolledSet, x => x.StudentId) .Select(x => new { x.StudentId, x.Student!.StudentNumber, x.Student.Name, ClassName = x.Student.AdministrativeClass!.Name, x.Id }) .ToListAsync(cancellationToken); foreach (var s in deferredStudents) { if (!results.ContainsKey(s.StudentId)) { results[s.StudentId] = new EligibleStudent( s.StudentId, s.StudentNumber, s.Name, s.ClassName, MakeupReason.DeferredApproved, null, SourceDeferredExamId: s.Id); } } // 2. Absent students var absentStudents = await db.GradeRecords.AsNoTracking() .Where(x => x.GradeSheetId == gradeSheet.Id && x.ExamStatus == GradeExamStatus.Absent) .WhereNotIn(enrolledSet, x => x.StudentId) .Select(x => new { x.StudentId, x.Student!.StudentNumber, x.Student.Name, ClassName = x.Student.AdministrativeClass!.Name, GradeRecordId = (Guid?)x.Id }) .ToListAsync(cancellationToken); foreach (var s in absentStudents) { if (!results.ContainsKey(s.StudentId)) { results[s.StudentId] = new EligibleStudent( s.StudentId, s.StudentNumber, s.Name, s.ClassName, MakeupReason.Absent, null, SourceGradeRecordId: s.GradeRecordId); } } // 3. Failed students (TotalScore < 60, Normal status) var failedStudents = await db.GradeRecords.AsNoTracking() .Where(x => x.GradeSheetId == gradeSheet.Id && x.ExamStatus == GradeExamStatus.Normal && x.TotalScore < 60) .WhereNotIn(enrolledSet, x => x.StudentId) .Select(x => new { x.StudentId, x.Student!.StudentNumber, x.Student.Name, ClassName = x.Student.AdministrativeClass!.Name, x.TotalScore, GradeRecordId = (Guid?)x.Id }) .ToListAsync(cancellationToken); foreach (var s in failedStudents) { if (!results.ContainsKey(s.StudentId)) { results[s.StudentId] = new EligibleStudent( s.StudentId, s.StudentNumber, s.Name, s.ClassName, MakeupReason.Failed, s.TotalScore, SourceGradeRecordId: s.GradeRecordId); } } return [.. results.Values.OrderBy(x => x.StudentNumber)]; } } public sealed record EligibleStudent( Guid StudentId, string StudentNumber, string Name, string ClassName, MakeupReason Reason, decimal? TotalScore = null, Guid? SourceGradeRecordId = null, Guid? SourceDeferredExamId = null, string? Detail = null);