From be2f738e586f8795a54ab60f2f2ba9043bec626d Mon Sep 17 00:00:00 2001 From: peter Date: Tue, 20 Jan 2026 22:07:23 +0100 Subject: [PATCH] Deduplicate penalties for same company appearing multiple times When a company appears multiple times in employment history (e.g., multiple roles at same company), penalties are now applied only once per unique company, not per employment entry. - Unverified company: -10 pts once per company (not per role) - Company flags (incorporation date, etc.): once per (company, flag type) Description now shows "(X roles)" when multiple instances exist. Co-Authored-By: Claude Opus 4.5 --- .../Jobs/ProcessCVCheckJob.cs | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/TrueCV.Infrastructure/Jobs/ProcessCVCheckJob.cs b/src/TrueCV.Infrastructure/Jobs/ProcessCVCheckJob.cs index 70c517c..cfc6cf5 100644 --- a/src/TrueCV.Infrastructure/Jobs/ProcessCVCheckJob.cs +++ b/src/TrueCV.Infrastructure/Jobs/ProcessCVCheckJob.cs @@ -261,26 +261,48 @@ public sealed class ProcessCVCheckJob var score = BaseScore; var flags = new List(); - // Penalty for unverified companies - foreach (var verification in verifications.Where(v => !v.IsVerified)) + // Penalty for unverified companies (deduplicated by company name) + var unverifiedByCompany = verifications + .Where(v => !v.IsVerified) + .GroupBy(v => v.ClaimedCompany, StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var companyGroup in unverifiedByCompany) { score -= UnverifiedCompanyPenalty; + var firstInstance = companyGroup.First(); + var instanceCount = companyGroup.Count(); + + var description = instanceCount > 1 + ? $"Could not verify employment at '{firstInstance.ClaimedCompany}' ({instanceCount} roles). {firstInstance.VerificationNotes}" + : $"Could not verify employment at '{firstInstance.ClaimedCompany}'. {firstInstance.VerificationNotes}"; flags.Add(new FlagResult { Category = FlagCategory.Employment.ToString(), Severity = FlagSeverity.Warning.ToString(), Title = "Unverified Company", - Description = $"Could not verify employment at '{verification.ClaimedCompany}'. {verification.VerificationNotes}", + Description = description, ScoreImpact = -UnverifiedCompanyPenalty }); } // Process company verification flags (incorporation date, dissolution, dormant, etc.) + // Deduplicate by (company, flag type) to avoid penalizing same issue multiple times + var processedCompanyFlags = new HashSet<(string Company, string FlagType)>( + new CompanyFlagComparer()); + foreach (var verification in verifications.Where(v => v.Flags.Count > 0)) { foreach (var companyFlag in verification.Flags) { + var key = (verification.ClaimedCompany, companyFlag.Type); + if (!processedCompanyFlags.Add(key)) + { + // Already processed this flag for this company, skip + continue; + } + var penalty = Math.Abs(companyFlag.ScoreImpact); score -= penalty; @@ -660,4 +682,24 @@ public sealed class ProcessCVCheckJob // Level 1: Junior / Entry-level return 1; } + + /// + /// Comparer for deduplicating company flags by (company name, flag type). + /// Uses case-insensitive comparison for company names. + /// + private sealed class CompanyFlagComparer : IEqualityComparer<(string Company, string FlagType)> + { + public bool Equals((string Company, string FlagType) x, (string Company, string FlagType) y) + { + return string.Equals(x.Company, y.Company, StringComparison.OrdinalIgnoreCase) && + string.Equals(x.FlagType, y.FlagType, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode((string Company, string FlagType) obj) + { + return HashCode.Combine( + obj.Company?.ToUpperInvariant() ?? "", + obj.FlagType?.ToUpperInvariant() ?? ""); + } + } }