using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Infrastructure.Scheduling; namespace Jiaowu.Api.Infrastructure.CourseSelection; public static class CourseSelectionRules { public static bool IsSelectionOpen(CourseSelectionRound round, DateTime nowUtc) => round.Status == CourseSelectionRoundStatus.Open && nowUtc >= round.StartsAt && nowUtc <= round.EndsAt; public static bool CanWithdraw(CourseSelectionRound round, DateTime nowUtc) => round.Status == CourseSelectionRoundStatus.Open && nowUtc <= round.WithdrawalEndsAt; public static bool CanPromoteWaitlist(CourseSelectionRound round, DateTime nowUtc) => round.Status == CourseSelectionRoundStatus.Open && nowUtc <= round.WithdrawalEndsAt; public static bool SupportsProxyEnrollment(CourseNature nature) => nature == CourseNature.GeneralRequired; public static bool RequiresPublishedSchedule(TeachingTaskSchedulingMode schedulingMode) => schedulingMode == TeachingTaskSchedulingMode.Standard; public static bool HasScheduleConflict( IEnumerable candidateEntries, IEnumerable selectedEntries) => candidateEntries.Any(candidate => selectedEntries.Any(selected => ScheduleConflictDetector.TimeOverlaps(candidate, selected))); /// /// Returns the overlap percentage of candidate schedule entries with selected entries. /// 0 = no conflict, 100 = fully overlapping. /// Used for retake enrollment where ≤50% overlap is allowed. /// public static double CalculateScheduleOverlap( IReadOnlyCollection candidateEntries, IReadOnlyCollection selectedEntries) { if (candidateEntries.Count == 0 || selectedEntries.Count == 0) return 0; int totalCandidatePeriods = 0; int overlappedPeriods = 0; foreach (var candidate in candidateEntries) { totalCandidatePeriods += candidate.PeriodCount; foreach (var selected in selectedEntries) { if (!ScheduleConflictDetector.TimeOverlaps(candidate, selected)) continue; // Calculate overlapping periods var overlapStart = Math.Max( candidate.StartPeriod, selected.StartPeriod); var overlapEnd = Math.Min( candidate.StartPeriod + candidate.PeriodCount, selected.StartPeriod + selected.PeriodCount); if (overlapEnd > overlapStart) overlappedPeriods += overlapEnd - overlapStart; } } return totalCandidatePeriods == 0 ? 0 : (double)overlappedPeriods / totalCandidatePeriods * 100; } /// Retake expanded capacity: ceiling(original * 1.15). public static int RetakeCapacity(int originalCapacity) => (int)Math.Ceiling(originalCapacity * 1.15); public static int EffectiveCapacity(int originalCapacity, bool isRetake) => isRetake ? RetakeCapacity(originalCapacity) : originalCapacity; }