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 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 22:07:23 +01:00
parent 7f4c2362f0
commit be2f738e58

View File

@@ -261,26 +261,48 @@ public sealed class ProcessCVCheckJob
var score = BaseScore; var score = BaseScore;
var flags = new List<FlagResult>(); var flags = new List<FlagResult>();
// Penalty for unverified companies // Penalty for unverified companies (deduplicated by company name)
foreach (var verification in verifications.Where(v => !v.IsVerified)) var unverifiedByCompany = verifications
.Where(v => !v.IsVerified)
.GroupBy(v => v.ClaimedCompany, StringComparer.OrdinalIgnoreCase)
.ToList();
foreach (var companyGroup in unverifiedByCompany)
{ {
score -= UnverifiedCompanyPenalty; 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 flags.Add(new FlagResult
{ {
Category = FlagCategory.Employment.ToString(), Category = FlagCategory.Employment.ToString(),
Severity = FlagSeverity.Warning.ToString(), Severity = FlagSeverity.Warning.ToString(),
Title = "Unverified Company", Title = "Unverified Company",
Description = $"Could not verify employment at '{verification.ClaimedCompany}'. {verification.VerificationNotes}", Description = description,
ScoreImpact = -UnverifiedCompanyPenalty ScoreImpact = -UnverifiedCompanyPenalty
}); });
} }
// Process company verification flags (incorporation date, dissolution, dormant, etc.) // 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 verification in verifications.Where(v => v.Flags.Count > 0))
{ {
foreach (var companyFlag in verification.Flags) 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); var penalty = Math.Abs(companyFlag.ScoreImpact);
score -= penalty; score -= penalty;
@@ -660,4 +682,24 @@ public sealed class ProcessCVCheckJob
// Level 1: Junior / Entry-level // Level 1: Junior / Entry-level
return 1; return 1;
} }
/// <summary>
/// Comparer for deduplicating company flags by (company name, flag type).
/// Uses case-insensitive comparison for company names.
/// </summary>
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() ?? "");
}
}
} }