using RealCV.Application.Data; using RealCV.Application.Interfaces; using RealCV.Application.Models; namespace RealCV.Infrastructure.Services; public sealed class EducationVerifierService : IEducationVerifierService { private const int MinimumDegreeYears = 1; private const int MaximumDegreeYears = 8; private const int MinimumGraduationAge = 18; public EducationVerificationResult Verify(EducationEntry education) { var institution = education.Institution; // Check for unaccredited institution first (highest priority flag) if (UnaccreditedInstitutions.IsUnaccredited(institution)) { return new EducationVerificationResult { ClaimedInstitution = institution, Status = "Unaccredited", IsVerified = false, IsUnaccredited = true, IsSuspicious = true, VerificationNotes = "Institution not found in QAA/HESA register of recognised institutions", ClaimedStartDate = education.StartDate, ClaimedEndDate = education.EndDate, DatesArePlausible = true, ClaimedQualification = education.Qualification, ClaimedSubject = education.Subject }; } // Check for suspicious patterns if (UnaccreditedInstitutions.HasSuspiciousPattern(institution)) { return new EducationVerificationResult { ClaimedInstitution = institution, Status = "Suspicious", IsVerified = false, IsUnaccredited = false, IsSuspicious = true, VerificationNotes = "Institution name contains patterns that may indicate unaccredited status", ClaimedStartDate = education.StartDate, ClaimedEndDate = education.EndDate, DatesArePlausible = true, ClaimedQualification = education.Qualification, ClaimedSubject = education.Subject }; } // Check if it's a recognised UK institution var officialName = UKInstitutions.GetOfficialName(institution); if (officialName != null) { var (datesPlausible, dateNotes) = CheckDatePlausibility(education.StartDate, education.EndDate); return new EducationVerificationResult { ClaimedInstitution = institution, MatchedInstitution = officialName, Status = "Recognised", IsVerified = true, IsUnaccredited = false, IsSuspicious = false, VerificationNotes = institution.Equals(officialName, StringComparison.OrdinalIgnoreCase) ? "Verified UK higher education institution" : $"Matched to official name: {officialName}", ClaimedStartDate = education.StartDate, ClaimedEndDate = education.EndDate, DatesArePlausible = datesPlausible, DatePlausibilityNotes = dateNotes, ClaimedQualification = education.Qualification, ClaimedSubject = education.Subject }; } // Check if this looks like a UK university name but isn't recognised // This catches fake institutions like "University of the Peak District" if (LooksLikeUKUniversity(institution)) { return new EducationVerificationResult { ClaimedInstitution = institution, Status = "Suspicious", IsVerified = false, IsUnaccredited = false, IsSuspicious = true, VerificationNotes = "Institution uses UK university naming convention but is not found in the register of recognised UK institutions", ClaimedStartDate = education.StartDate, ClaimedEndDate = education.EndDate, DatesArePlausible = true, ClaimedQualification = education.Qualification, ClaimedSubject = education.Subject }; } // Not in our database - could be international or unrecognised var isUnknownInstitution = string.IsNullOrWhiteSpace(institution) || institution.Equals("Unknown Institution", StringComparison.OrdinalIgnoreCase) || institution.Equals("Unknown", StringComparison.OrdinalIgnoreCase); return new EducationVerificationResult { ClaimedInstitution = institution, Status = "Unknown", IsVerified = false, IsUnaccredited = false, IsSuspicious = false, VerificationNotes = isUnknownInstitution ? null : "Institution not found in UK recognised institutions database. May be an international institution.", ClaimedStartDate = education.StartDate, ClaimedEndDate = education.EndDate, DatesArePlausible = true, ClaimedQualification = education.Qualification, ClaimedSubject = education.Subject }; } /// /// Checks if an institution name follows UK university naming conventions. /// If it does but isn't in the recognised list, it's likely a fake UK institution. /// private static bool LooksLikeUKUniversity(string? institution) { if (string.IsNullOrWhiteSpace(institution)) return false; var lower = institution.ToLowerInvariant().Trim(); // Skip if explicitly marked as foreign/international if (lower.Contains("foreign") || lower.Contains("international")) return false; // "University of the [X]" is a distinctly British naming pattern // Examples: University of the West of England, University of the Highlands and Islands // Fake examples: University of the Peak District, University of the Cotswolds if (lower.StartsWith("university of the ")) return true; // UK-specific naming patterns that are less common internationally if (lower.Contains(" metropolitan university")) // Manchester Metropolitan University return true; if (lower.Contains(" brookes university")) // Oxford Brookes return true; if (lower.Contains(" hallam university")) // Sheffield Hallam return true; if (lower.Contains(" beckett university")) // Leeds Beckett return true; if (lower.Contains(" napier university")) // Edinburgh Napier return true; if (lower.Contains(" trent university")) // Nottingham Trent return true; if (lower.StartsWith("royal college of ")) // Royal College of Art, etc. return true; // Check for UK place names that don't have real universities // These are well-known UK regions/places used by diploma mills var fakeUkPatterns = new[] { "university of devonshire", "university of cornwall", // No "University of Cornwall" - only Falmouth "university of wiltshire", "university of dorset", "university of hampshire", "university of norfolk", "university of suffolk", // Note: There IS a University of Suffolk now "university of berkshire", "university of shropshire", "university of herefordshire", "university of rutland", "university of cumbria", // This one exists - keep for now "university of england", "university of britain", "university of the lake district", "university of the cotswolds", "university of the peak district", "university of the dales", "university of the moors", "university of the fens", "university of london south", "university of london north", "university of london east", "university of london west", }; foreach (var pattern in fakeUkPatterns) { if (lower.Contains(pattern)) return true; } return false; } public List VerifyAll( List education, List? employment = null) { var results = new List(); foreach (var edu in education) { var result = Verify(edu); // If we have employment data, check for timeline issues if (employment?.Count > 0 && result.ClaimedEndDate.HasValue) { var (timelinePlausible, timelineNotes) = CheckEducationEmploymentTimeline( result.ClaimedEndDate.Value, employment); if (!timelinePlausible) { result = result with { DatesArePlausible = false, DatePlausibilityNotes = CombineNotes(result.DatePlausibilityNotes, timelineNotes) }; } } results.Add(result); } // Check for overlapping education periods CheckOverlappingEducation(results); return results; } private static (bool isPlausible, string? notes) CheckDatePlausibility(DateOnly? startDate, DateOnly? endDate) { if (!startDate.HasValue || !endDate.HasValue) { return (true, null); } var start = startDate.Value; var end = endDate.Value; // End date should be after start date if (end <= start) { return (false, "End date is before or equal to start date"); } // Check course duration is reasonable var years = (end.ToDateTime(TimeOnly.MinValue) - start.ToDateTime(TimeOnly.MinValue)).TotalDays / 365.25; if (years < MinimumDegreeYears) { return (false, $"Course duration ({years:F1} years) is unusually short for a degree"); } if (years > MaximumDegreeYears) { return (false, $"Course duration ({years:F1} years) is unusually long"); } // Check if graduation date is in the future if (end > DateOnly.FromDateTime(DateTime.UtcNow)) { return (true, "Graduation date is in the future - possibly currently studying"); } return (true, null); } private static (bool isPlausible, string? notes) CheckEducationEmploymentTimeline( DateOnly graduationDate, List employment) { // Find the earliest employment start date var earliestEmployment = employment .Where(e => e.StartDate.HasValue) .OrderBy(e => e.StartDate) .FirstOrDefault(); if (earliestEmployment?.StartDate == null) { return (true, null); } var employmentStart = earliestEmployment.StartDate.Value; // If someone claims to have started full-time work significantly before graduating, // that's suspicious (unless it's clearly an internship/part-time role) var monthsBeforeGraduation = (graduationDate.ToDateTime(TimeOnly.MinValue) - employmentStart.ToDateTime(TimeOnly.MinValue)).TotalDays / 30; if (monthsBeforeGraduation > 24) // More than 2 years before graduation { var isLikelyInternship = earliestEmployment.JobTitle.Contains("intern", StringComparison.OrdinalIgnoreCase) || earliestEmployment.JobTitle.Contains("placement", StringComparison.OrdinalIgnoreCase) || earliestEmployment.JobTitle.Contains("trainee", StringComparison.OrdinalIgnoreCase); if (!isLikelyInternship) { return (false, $"Employment at {earliestEmployment.CompanyName} started {monthsBeforeGraduation:F0} months before claimed graduation"); } } return (true, null); } private static void CheckOverlappingEducation(List results) { var datedResults = results .Where(r => r.ClaimedStartDate.HasValue && r.ClaimedEndDate.HasValue) .ToList(); for (var i = 0; i < datedResults.Count; i++) { for (var j = i + 1; j < datedResults.Count; j++) { var edu1 = datedResults[i]; var edu2 = datedResults[j]; if (PeriodsOverlap( edu1.ClaimedStartDate!.Value, edu1.ClaimedEndDate!.Value, edu2.ClaimedStartDate!.Value, edu2.ClaimedEndDate!.Value)) { // Find the actual index in the original results list var idx1 = results.IndexOf(edu1); var idx2 = results.IndexOf(edu2); if (idx1 >= 0) { results[idx1] = edu1 with { DatePlausibilityNotes = CombineNotes( edu1.DatePlausibilityNotes, $"Overlaps with education at {edu2.ClaimedInstitution}") }; } if (idx2 >= 0) { results[idx2] = edu2 with { DatePlausibilityNotes = CombineNotes( edu2.DatePlausibilityNotes, $"Overlaps with education at {edu1.ClaimedInstitution}") }; } } } } } private static bool PeriodsOverlap(DateOnly start1, DateOnly end1, DateOnly start2, DateOnly end2) { return start1 < end2 && start2 < end1; } private static string? CombineNotes(string? existing, string? additional) { if (string.IsNullOrEmpty(additional)) return existing; if (string.IsNullOrEmpty(existing)) return additional; return $"{existing}; {additional}"; } }