namespace JobTrackerApi.Services { public enum PipelineCategory { Active, Success, Closed, } public sealed record PipelineStage(string Key, int Order, PipelineCategory Category); /// /// Canonical job-application pipeline: the single source of truth for the ordered set of /// statuses, their grouping, and how free-text/legacy values normalize onto them. /// Status remains a free-text column so custom values are never destroyed; this only /// canonicalizes casing and known synonyms. /// public static class JobPipeline { public const string DefaultStatus = "Applied"; public static readonly IReadOnlyList Stages = new List { new("Applied", 1, PipelineCategory.Active), new("Waiting", 2, PipelineCategory.Active), new("Interview", 3, PipelineCategory.Active), new("Offer", 4, PipelineCategory.Success), new("Rejected", 5, PipelineCategory.Closed), new("Ghosted", 6, PipelineCategory.Closed), }; private static readonly Dictionary Canonical = Stages.ToDictionary(s => s.Key, s => s.Key, StringComparer.OrdinalIgnoreCase); // Legacy/synonym spellings that should collapse onto a canonical stage. private static readonly Dictionary Aliases = new(StringComparer.OrdinalIgnoreCase) { ["interviewing"] = "Interview", ["interviews"] = "Interview", ["interviewed"] = "Interview", ["in interview"] = "Interview", ["awaiting response"] = "Waiting", ["awaiting"] = "Waiting", ["in progress"] = "Waiting", ["pending"] = "Waiting", ["no response"] = "Ghosted", ["no reply"] = "Ghosted", ["declined"] = "Rejected", }; /// /// Returns the canonical status for a raw value: trims, matches a stage case-insensitively, /// or maps a known synonym. Unknown non-empty values are preserved (trimmed) so custom /// statuses survive. Empty/whitespace becomes the default stage. /// public static string Normalize(string? status) { var trimmed = (status ?? string.Empty).Trim(); if (trimmed.Length == 0) return DefaultStatus; if (Canonical.TryGetValue(trimmed, out var canonical)) return canonical; if (Aliases.TryGetValue(trimmed, out var alias)) return alias; return trimmed; } public static bool IsCanonical(string? status) => !string.IsNullOrWhiteSpace(status) && Canonical.ContainsKey(status.Trim()); public static int OrderOf(string? status) { var normalized = Normalize(status); var stage = Stages.FirstOrDefault(s => string.Equals(s.Key, normalized, StringComparison.OrdinalIgnoreCase)); return stage?.Order ?? int.MaxValue; // custom statuses sort last } } }