namespace JobTrackerApi.Services { public sealed record EmailStatusSuggestion(string SuggestedStatus, string Signal, string Confidence); /// /// Deterministic email → pipeline-status classifier. Scans subject/body for outcome signals and /// suggests a canonical status. No AI: instant, reproducible, and safe (the user always confirms). /// Priority matters — a rejection email often still mentions "interview", so rejection wins. /// public static class EmailStatusClassifier { // Ordered highest-priority first. Each stage lists lowercase phrases to look for. private static readonly (string Status, string Confidence, string[] Phrases)[] Rules = { ("Rejected", "high", new[] { "regret to inform", "we regret", "unfortunately, we", "not moving forward", "not be moving forward", "decided not to proceed", "will not be proceeding", "not to proceed", "not been selected", "will not be progressing", "unable to offer", "position has been filled", "no longer being considered", "decided to move forward with other", "pursue other candidates", "not to move forward", "were not successful", "was not successful", }), ("Offer", "high", new[] { "pleased to offer", "delighted to offer", "happy to offer", "offer of employment", "job offer", "we would like to offer", "formal offer", "extend an offer", "offer letter", "excited to offer", }), ("Interview", "medium", new[] { "invite you to interview", "invite you to an interview", "schedule an interview", "would like to invite you", "phone screen", "phone interview", "video interview", "technical interview", "next steps in the", "your availability for a call", "availability for an interview", "set up a call", "set up an interview", "meet the team", "book a time", "invitation to interview", "interview invitation", "like to speak with you", "move to the interview", }), }; // Weaker single-word cues only fire when no strong phrase matched (kept low-confidence). private static readonly string[] InterviewWeakCues = { "interview", "assessment", "coding challenge", "take-home" }; public static EmailStatusSuggestion? Classify(string? subject, string? body) { var text = $"{subject}\n{body}".ToLowerInvariant(); if (string.IsNullOrWhiteSpace(text)) return null; foreach (var (status, confidence, phrases) in Rules) { var hit = phrases.FirstOrDefault(p => text.Contains(p, StringComparison.Ordinal)); if (hit is not null) return new EmailStatusSuggestion(status, hit, confidence); } var weak = InterviewWeakCues.FirstOrDefault(c => text.Contains(c, StringComparison.Ordinal)); if (weak is not null) return new EmailStatusSuggestion("Interview", weak, "low"); return null; } } }