using PowderCoating.Core.Entities; using PowderCoating.Shared.Constants; namespace PowderCoating.Web.Controllers; /// Risk bucket for a tenant company, derived from its health score. public enum ChurnRisk { Healthy, AtRisk, Critical, NeverActivated } /// /// Shared health-score logic used by both (dashboard) /// and (list + detail badges). /// public static class CompanyHealthHelper { /// /// Computes a 0–100 health score and a list of human-readable risk signals for a single /// company based on its subscription status, login recency, and job activity. /// See XML doc for scoring rules. /// public static (int Score, List Signals) ComputeHealth( Company c, int daysSinceLogin, int j30, int j90, int totalJobs, DateTime now) { var score = 100; var signals = new List(); if (!c.IsActive) { signals.Add("Account disabled"); return (0, signals); } if (!c.IsComped && c.SubscriptionEndDate.HasValue) { var daysUntil = (int)(c.SubscriptionEndDate.Value.Date - now.Date).TotalDays; if (daysUntil < -AppConstants.SubscriptionConstants.GracePeriodDays) { score -= 50; signals.Add("Subscription expired"); } else if (daysUntil < 0) { score -= 30; signals.Add("In grace period"); } else if (daysUntil <= 7) { score -= 20; signals.Add($"Expires in {daysUntil}d"); } else if (daysUntil <= 14) { score -= 10; signals.Add($"Expires in {daysUntil}d"); } } if (daysSinceLogin == -1) { score -= 30; signals.Add("Never logged in"); } else if (daysSinceLogin >= 90) { score -= 30; signals.Add($"No login {daysSinceLogin}d"); } else if (daysSinceLogin >= 60) { score -= 20; signals.Add($"No login {daysSinceLogin}d"); } else if (daysSinceLogin >= 30) { score -= 10; signals.Add($"No login {daysSinceLogin}d"); } if (totalJobs == 0) { score -= 20; signals.Add("No jobs ever"); } else if (j90 == 0) { score -= 10; signals.Add("No jobs in 90d"); } else if (j30 == 0) { score -= 5; signals.Add("No jobs in 30d"); } return (Math.Max(0, score), signals); } /// /// Derives a bucket from a pre-computed score and activity flags. /// public static ChurnRisk ToRiskLevel(int score, bool neverActivated) => neverActivated ? ChurnRisk.NeverActivated : score >= 75 ? ChurnRisk.Healthy : score >= 45 ? ChurnRisk.AtRisk : ChurnRisk.Critical; }