Add comprehensive CV verification checks and dashboard auto-refresh

- Add dashboard auto-refresh polling to update when processing completes
- Skip verification for freelance employers (but not contractors)
- Add incorporation date check (flags employment before company existed)
- Add dissolution date check (flags employment at dissolved companies)
- Add dormant company check (flags non-director roles at dormant companies)
- Add company size vs role check (flags senior roles at micro-entities)
- Add SIC code mismatch check (flags tech roles at non-tech companies)
- Add director verification against Companies House officers
- Add rapid career progression detection (3+ seniority jumps in <2 years)
- Add early career senior role detection (<2 years after education)
- Extend CompanyVerificationResult with flags and company data
- Add officers endpoint to Companies House client
- Fix null reference warning in Report.razor

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 20:00:24 +01:00
parent acf4d96fae
commit 652aa2e612
9 changed files with 937 additions and 36 deletions

View File

@@ -5,6 +5,7 @@ namespace TrueCV.Application.Interfaces;
public interface ICompanyVerifierService public interface ICompanyVerifierService
{ {
Task<CompanyVerificationResult> VerifyCompanyAsync(string companyName, DateOnly? startDate, DateOnly? endDate); Task<CompanyVerificationResult> VerifyCompanyAsync(string companyName, DateOnly? startDate, DateOnly? endDate, string? jobTitle = null);
Task<List<CompanySearchResult>> SearchCompaniesAsync(string query); Task<List<CompanySearchResult>> SearchCompaniesAsync(string query);
Task<bool?> VerifyDirectorAsync(string companyNumber, string candidateName, DateOnly? startDate, DateOnly? endDate);
} }

View File

@@ -10,4 +10,26 @@ public sealed record CompanyVerificationResult
public string? VerificationNotes { get; init; } public string? VerificationNotes { get; init; }
public DateOnly? ClaimedStartDate { get; init; } public DateOnly? ClaimedStartDate { get; init; }
public DateOnly? ClaimedEndDate { get; init; } public DateOnly? ClaimedEndDate { get; init; }
public string? CompanyType { get; init; }
public string? ClaimedJobTitle { get; init; }
public bool? JobTitlePlausible { get; init; }
public string? JobTitleNotes { get; init; }
// Additional company data for verification checks
public string? CompanyStatus { get; init; }
public DateOnly? IncorporationDate { get; init; }
public DateOnly? DissolutionDate { get; init; }
public string? AccountsCategory { get; init; }
public List<string>? SicCodes { get; init; }
// Additional verification flags
public List<CompanyVerificationFlag> Flags { get; init; } = [];
}
public sealed record CompanyVerificationFlag
{
public required string Type { get; init; }
public required string Severity { get; init; }
public required string Message { get; init; }
public int ScoreImpact { get; init; }
} }

View File

@@ -16,9 +16,18 @@ public class CompanyCache
[MaxLength(64)] [MaxLength(64)]
public string Status { get; set; } = string.Empty; public string Status { get; set; } = string.Empty;
[MaxLength(64)]
public string? CompanyType { get; set; }
public DateOnly? IncorporationDate { get; set; } public DateOnly? IncorporationDate { get; set; }
public DateOnly? DissolutionDate { get; set; } public DateOnly? DissolutionDate { get; set; }
[MaxLength(64)]
public string? AccountsCategory { get; set; }
[MaxLength(256)]
public string? SicCodesJson { get; set; }
public DateTime CachedAt { get; set; } public DateTime CachedAt { get; set; }
} }

View File

@@ -117,6 +117,50 @@ public sealed class CompaniesHouseClient
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.", ex); throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.", ex);
} }
} }
public async Task<CompaniesHouseOfficersResponse?> GetOfficersAsync(
string companyNumber,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(companyNumber);
var requestUrl = $"/company/{companyNumber}/officers";
_logger.LogDebug("Fetching officers for company: {CompanyNumber}", companyNumber);
try
{
var response = await _httpClient.GetAsync(requestUrl, cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
_logger.LogDebug("No officers found for company: {CompanyNumber}", companyNumber);
return null;
}
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.LogWarning("Rate limit exceeded for Companies House API");
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.");
}
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<CompaniesHouseOfficersResponse>(
JsonOptions,
cancellationToken);
_logger.LogDebug("Retrieved {Count} officers for company {CompanyNumber}",
result?.Items?.Count ?? 0, companyNumber);
return result;
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.LogWarning("Rate limit exceeded for Companies House API");
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.", ex);
}
}
} }
// DTOs for Companies House API responses // DTOs for Companies House API responses
@@ -138,6 +182,7 @@ public sealed record CompaniesHouseSearchItem
public string? DateOfCessation { get; init; } public string? DateOfCessation { get; init; }
public CompaniesHouseAddress? Address { get; init; } public CompaniesHouseAddress? Address { get; init; }
public string? AddressSnippet { get; init; } public string? AddressSnippet { get; init; }
public List<string>? SicCodes { get; init; }
} }
public sealed record CompaniesHouseCompany public sealed record CompaniesHouseCompany
@@ -149,6 +194,34 @@ public sealed record CompaniesHouseCompany
public string? DateOfCreation { get; init; } public string? DateOfCreation { get; init; }
public string? DateOfCessation { get; init; } public string? DateOfCessation { get; init; }
public CompaniesHouseAddress? RegisteredOfficeAddress { get; init; } public CompaniesHouseAddress? RegisteredOfficeAddress { get; init; }
public List<string>? SicCodes { get; init; }
public CompaniesHouseAccounts? Accounts { get; init; }
}
public sealed record CompaniesHouseAccounts
{
public string? AccountingReferenceDate { get; init; }
public CompaniesHouseLastAccounts? LastAccounts { get; init; }
}
public sealed record CompaniesHouseLastAccounts
{
public string? MadeUpTo { get; init; }
public string? Type { get; init; } // e.g., "micro-entity", "small", "medium", "dormant", etc.
}
public sealed record CompaniesHouseOfficersResponse
{
public int TotalResults { get; init; }
public List<CompaniesHouseOfficer> Items { get; init; } = [];
}
public sealed record CompaniesHouseOfficer
{
public required string Name { get; init; }
public string? OfficerRole { get; init; }
public string? AppointedOn { get; init; }
public string? ResignedOn { get; init; }
} }
public sealed record CompaniesHouseAddress public sealed record CompaniesHouseAddress

View File

@@ -22,6 +22,10 @@ public sealed class ProcessCVCheckJob
private const int BaseScore = 100; private const int BaseScore = 100;
private const int UnverifiedCompanyPenalty = 10; private const int UnverifiedCompanyPenalty = 10;
private const int ImplausibleJobTitlePenalty = 15;
private const int CompanyVerificationFlagPenalty = 5; // Base penalty for company flags, actual from flag.ScoreImpact
private const int RapidProgressionPenalty = 10;
private const int EarlyCareerSeniorRolePenalty = 10;
private const int GapMonthPenalty = 1; private const int GapMonthPenalty = 1;
private const int MaxGapPenalty = 10; private const int MaxGapPenalty = 10;
private const int OverlapMonthPenalty = 2; private const int OverlapMonthPenalty = 2;
@@ -86,22 +90,46 @@ public sealed class ProcessCVCheckJob
await _dbContext.SaveChangesAsync(cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken);
// Step 5: Verify each employment entry (parallelized with rate limiting) // Step 5: Verify each employment entry (parallelized with rate limiting)
var verificationTasks = cvData.Employment.Select(async employment => // Skip freelance entries as they cannot be verified against company registries
{ var verificationTasks = cvData.Employment
var result = await _companyVerifierService.VerifyCompanyAsync( .Where(e => !IsFreelance(e.CompanyName))
employment.CompanyName, .Select(async employment =>
employment.StartDate, {
employment.EndDate); var result = await _companyVerifierService.VerifyCompanyAsync(
employment.CompanyName,
employment.StartDate,
employment.EndDate,
employment.JobTitle);
_logger.LogDebug( _logger.LogDebug(
"Verified {Company}: {IsVerified} (Score: {Score}%)", "Verified {Company}: {IsVerified} (Score: {Score}%), JobTitle: {JobTitle}, Plausible: {Plausible}",
employment.CompanyName, result.IsVerified, result.MatchScore); employment.CompanyName, result.IsVerified, result.MatchScore,
employment.JobTitle, result.JobTitlePlausible);
return result; return result;
}); });
var verificationResults = (await Task.WhenAll(verificationTasks)).ToList(); var verificationResults = (await Task.WhenAll(verificationTasks)).ToList();
// Add freelance entries as auto-verified (skipped)
foreach (var employment in cvData.Employment.Where(e => IsFreelance(e.CompanyName)))
{
verificationResults.Add(new CompanyVerificationResult
{
ClaimedCompany = employment.CompanyName,
IsVerified = true,
MatchScore = 100,
VerificationNotes = "Freelance/self-employed - verification skipped",
ClaimedJobTitle = employment.JobTitle,
JobTitlePlausible = true
});
_logger.LogDebug("Skipped verification for freelance entry: {Company}", employment.CompanyName);
}
// Step 5b: Verify director claims against Companies House officers
await VerifyDirectorClaims(cvData.FullName, verificationResults, cancellationToken);
// Step 6: Verify education entries // Step 6: Verify education entries
var educationResults = _educationVerifierService.VerifyAll( var educationResults = _educationVerifierService.VerifyAll(
cvData.Education, cvData.Education,
@@ -122,7 +150,7 @@ public sealed class ProcessCVCheckJob
cvCheckId, timelineAnalysis.Gaps.Count, timelineAnalysis.Overlaps.Count); cvCheckId, timelineAnalysis.Gaps.Count, timelineAnalysis.Overlaps.Count);
// Step 8: Calculate veracity score // Step 8: Calculate veracity score
var (score, flags) = CalculateVeracityScore(verificationResults, educationResults, timelineAnalysis); var (score, flags) = CalculateVeracityScore(verificationResults, educationResults, timelineAnalysis, cvData);
_logger.LogDebug("Calculated veracity score for check {CheckId}: {Score}", cvCheckId, score); _logger.LogDebug("Calculated veracity score for check {CheckId}: {Score}", cvCheckId, score);
@@ -194,7 +222,8 @@ public sealed class ProcessCVCheckJob
private static (int Score, List<FlagResult> Flags) CalculateVeracityScore( private static (int Score, List<FlagResult> Flags) CalculateVeracityScore(
List<CompanyVerificationResult> verifications, List<CompanyVerificationResult> verifications,
List<EducationVerificationResult> educationResults, List<EducationVerificationResult> educationResults,
TimelineAnalysisResult timeline) TimelineAnalysisResult timeline,
CVData cvData)
{ {
var score = BaseScore; var score = BaseScore;
var flags = new List<FlagResult>(); var flags = new List<FlagResult>();
@@ -214,6 +243,48 @@ public sealed class ProcessCVCheckJob
}); });
} }
// Process company verification flags (incorporation date, dissolution, dormant, etc.)
foreach (var verification in verifications.Where(v => v.Flags.Count > 0))
{
foreach (var companyFlag in verification.Flags)
{
var penalty = Math.Abs(companyFlag.ScoreImpact);
score -= penalty;
var severity = companyFlag.Severity switch
{
"Critical" => FlagSeverity.Critical,
"Warning" => FlagSeverity.Warning,
_ => FlagSeverity.Info
};
flags.Add(new FlagResult
{
Category = FlagCategory.Employment.ToString(),
Severity = severity.ToString(),
Title = companyFlag.Type switch
{
"EmploymentBeforeIncorporation" => "Employment Before Company Existed",
"EmploymentAtDissolvedCompany" => "Employment at Dissolved Company",
"CurrentEmploymentAtDissolvedCompany" => "Current Employment at Dissolved Company",
"EmploymentAtDormantCompany" => "Employment at Dormant Company",
"SeniorRoleAtMicroCompany" => "Senior Role at Micro Company",
"SicCodeMismatch" => "Role/Industry Mismatch",
"ImplausibleJobTitle" => "Implausible Job Title",
_ => companyFlag.Type
},
Description = companyFlag.Message,
ScoreImpact = -penalty
});
}
}
// Check for rapid career progression
CheckRapidCareerProgression(cvData.Employment, flags, ref score);
// Check for early career senior roles (relative to education end date)
CheckEarlyCareerSeniorRoles(cvData.Employment, cvData.Education, flags, ref score);
// Penalty for diploma mills (critical) // Penalty for diploma mills (critical)
foreach (var edu in educationResults.Where(e => e.IsDiplomaMill)) foreach (var edu in educationResults.Where(e => e.IsDiplomaMill))
{ {
@@ -328,4 +399,223 @@ public sealed class ProcessCVCheckJob
_ => "Very Poor" _ => "Very Poor"
}; };
} }
private static bool IsFreelance(string companyName)
{
if (string.IsNullOrWhiteSpace(companyName)) return false;
var name = companyName.Trim().ToLowerInvariant();
return name == "freelance" ||
name == "freelancer" ||
name == "self-employed" ||
name == "self employed" ||
name.StartsWith("freelance ") ||
name.StartsWith("self-employed ") ||
name.Contains("(freelance)") ||
name.Contains("(self-employed)");
}
private async Task VerifyDirectorClaims(
string candidateName,
List<CompanyVerificationResult> verificationResults,
CancellationToken cancellationToken)
{
// Find all director claims at verified companies
foreach (var result in verificationResults.Where(v => v.IsVerified && !string.IsNullOrEmpty(v.MatchedCompanyNumber)))
{
var jobTitle = result.ClaimedJobTitle?.ToLowerInvariant() ?? "";
// Check if this is a director claim
var isDirectorClaim = jobTitle.Contains("director") ||
jobTitle.Contains("company secretary") ||
jobTitle == "md" ||
jobTitle.Contains("managing director");
if (!isDirectorClaim) continue;
_logger.LogDebug(
"Verifying director claim for {Candidate} at {Company}",
candidateName, result.MatchedCompanyName);
var isVerifiedDirector = await _companyVerifierService.VerifyDirectorAsync(
result.MatchedCompanyNumber!,
candidateName,
result.ClaimedStartDate,
result.ClaimedEndDate);
if (isVerifiedDirector == false)
{
// Add a flag for unverified director claim
var flags = result.Flags.ToList();
flags.Add(new CompanyVerificationFlag
{
Type = "UnverifiedDirectorClaim",
Severity = "Critical",
Message = $"Claimed director role at '{result.MatchedCompanyName}' but candidate name not found in Companies House officers list",
ScoreImpact = -20
});
// Update the result with the new flag
var index = verificationResults.IndexOf(result);
verificationResults[index] = result with { Flags = flags };
_logger.LogWarning(
"Director claim not verified for {Candidate} at {Company}",
candidateName, result.MatchedCompanyName);
}
else if (isVerifiedDirector == true)
{
_logger.LogInformation(
"Director claim verified for {Candidate} at {Company}",
candidateName, result.MatchedCompanyName);
}
}
}
private static void CheckRapidCareerProgression(
List<EmploymentEntry> employment,
List<FlagResult> flags,
ref int score)
{
// Group employment by company and check for rapid promotions
var byCompany = employment
.Where(e => !string.IsNullOrWhiteSpace(e.CompanyName) && e.StartDate.HasValue)
.GroupBy(e => e.CompanyName.ToLowerInvariant())
.Where(g => g.Count() > 1);
foreach (var companyGroup in byCompany)
{
var orderedRoles = companyGroup.OrderBy(e => e.StartDate).ToList();
for (int i = 1; i < orderedRoles.Count; i++)
{
var prevRole = orderedRoles[i - 1];
var currRole = orderedRoles[i];
var prevSeniority = GetSeniorityLevel(prevRole.JobTitle);
var currSeniority = GetSeniorityLevel(currRole.JobTitle);
// Check for jump of 3+ seniority levels
var seniorityJump = currSeniority - prevSeniority;
if (seniorityJump >= 3)
{
// Calculate time between roles
var monthsBetween = ((currRole.StartDate!.Value.Year - prevRole.StartDate!.Value.Year) * 12) +
(currRole.StartDate!.Value.Month - prevRole.StartDate!.Value.Month);
// If jumped 3+ levels in less than 2 years, flag it
if (monthsBetween < 24)
{
score -= RapidProgressionPenalty;
flags.Add(new FlagResult
{
Category = FlagCategory.Employment.ToString(),
Severity = FlagSeverity.Warning.ToString(),
Title = "Rapid Career Progression",
Description = $"Promoted from '{prevRole.JobTitle}' to '{currRole.JobTitle}' at '{companyGroup.First().CompanyName}' in {monthsBetween} months - unusually fast progression",
ScoreImpact = -RapidProgressionPenalty
});
}
}
}
}
}
private static void CheckEarlyCareerSeniorRoles(
List<EmploymentEntry> employment,
List<EducationEntry> education,
List<FlagResult> flags,
ref int score)
{
// Find the latest education end date to estimate career start
var latestEducationEnd = education
.Where(e => e.EndDate.HasValue)
.Select(e => e.EndDate!.Value)
.DefaultIfEmpty(DateOnly.MinValue)
.Max();
if (latestEducationEnd == DateOnly.MinValue)
{
// No education dates available, skip check
return;
}
foreach (var emp in employment.Where(e => e.StartDate.HasValue))
{
var monthsAfterEducation = ((emp.StartDate!.Value.Year - latestEducationEnd.Year) * 12) +
(emp.StartDate!.Value.Month - latestEducationEnd.Month);
// Check if this is a senior role started within 2 years of finishing education
if (monthsAfterEducation < 24 && monthsAfterEducation >= 0)
{
var seniority = GetSeniorityLevel(emp.JobTitle);
// Flag if they're claiming a senior role (level 4+) very early in career
if (seniority >= 4)
{
score -= EarlyCareerSeniorRolePenalty;
flags.Add(new FlagResult
{
Category = FlagCategory.Employment.ToString(),
Severity = FlagSeverity.Warning.ToString(),
Title = "Early Career Senior Role",
Description = $"Claimed senior role '{emp.JobTitle}' at '{emp.CompanyName}' only {monthsAfterEducation} months after completing education",
ScoreImpact = -EarlyCareerSeniorRolePenalty
});
}
}
}
}
private static int GetSeniorityLevel(string? jobTitle)
{
if (string.IsNullOrWhiteSpace(jobTitle)) return 0;
var title = jobTitle.ToLowerInvariant();
// Level 6: C-suite
if (title.Contains("ceo") || title.Contains("cto") || title.Contains("cfo") ||
title.Contains("coo") || title.Contains("cio") || title.Contains("chief") ||
title.Contains("managing director") || title == "md" ||
title.Contains("president") || title.Contains("chairman") ||
title.Contains("chairwoman") || title.Contains("chairperson"))
{
return 6;
}
// Level 5: VP / Executive
if (title.Contains("vice president") || title.Contains("vp ") ||
title.StartsWith("vp") || title.Contains("svp") ||
title.Contains("executive director") || title.Contains("executive vice"))
{
return 5;
}
// Level 4: Director / Head
if (title.Contains("director") || title.Contains("head of"))
{
return 4;
}
// Level 3: Senior / Lead / Principal / Manager
if (title.Contains("senior") || title.Contains("lead") ||
title.Contains("principal") || title.Contains("manager") ||
title.Contains("team lead") || title.Contains("staff"))
{
return 3;
}
// Level 2: Mid-level (no junior, no senior)
if (!title.Contains("junior") && !title.Contains("trainee") &&
!title.Contains("intern") && !title.Contains("graduate") &&
!title.Contains("entry") && !title.Contains("assistant"))
{
return 2;
}
// Level 1: Junior / Entry-level
return 1;
}
} }

View File

@@ -1,3 +1,4 @@
using System.Text.Json;
using FuzzySharp; using FuzzySharp;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -20,6 +21,15 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
private const int FuzzyMatchThreshold = 70; private const int FuzzyMatchThreshold = 70;
private const int CacheExpirationDays = 30; private const int CacheExpirationDays = 30;
// SIC codes for tech/software companies
private static readonly HashSet<string> TechSicCodes = new()
{
"62011", "62012", "62020", "62030", "62090", // Computer programming and consultancy
"63110", "63120", // Data processing, hosting
"58210", "58290", // Publishing of computer games, other software
"61100", "61200", "61300", "61900" // Telecommunications
};
public CompanyVerifierService( public CompanyVerifierService(
CompaniesHouseClient companiesHouseClient, CompaniesHouseClient companiesHouseClient,
IDbContextFactory<ApplicationDbContext> dbContextFactory, IDbContextFactory<ApplicationDbContext> dbContextFactory,
@@ -33,18 +43,20 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
public async Task<CompanyVerificationResult> VerifyCompanyAsync( public async Task<CompanyVerificationResult> VerifyCompanyAsync(
string companyName, string companyName,
DateOnly? startDate, DateOnly? startDate,
DateOnly? endDate) DateOnly? endDate,
string? jobTitle = null)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(companyName); ArgumentException.ThrowIfNullOrWhiteSpace(companyName);
_logger.LogDebug("Verifying company: {CompanyName}", companyName); _logger.LogDebug("Verifying company: {CompanyName}", companyName);
var flags = new List<CompanyVerificationFlag>();
// Try to find a cached match first // Try to find a cached match first
var cachedMatch = await FindCachedMatchAsync(companyName); var cachedMatch = await FindCachedMatchAsync(companyName);
if (cachedMatch is not null) if (cachedMatch is not null)
{ {
_logger.LogDebug("Found cached company match for: {CompanyName}", companyName); _logger.LogDebug("Found cached company match for: {CompanyName}", companyName);
return CreateVerificationResult(companyName, cachedMatch, startDate, endDate); return CreateResultFromCache(cachedMatch, companyName, startDate, endDate, jobTitle, flags);
} }
// Search Companies House // Search Companies House
@@ -55,7 +67,7 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
if (searchResponse?.Items is null || searchResponse.Items.Count == 0) if (searchResponse?.Items is null || searchResponse.Items.Count == 0)
{ {
_logger.LogDebug("No companies found for: {CompanyName}", companyName); _logger.LogDebug("No companies found for: {CompanyName}", companyName);
return CreateUnverifiedResult(companyName, startDate, endDate, "No matching company found in Companies House"); return CreateUnverifiedResult(companyName, startDate, endDate, jobTitle, "No matching company found in Companies House");
} }
// Find best fuzzy match // Find best fuzzy match
@@ -64,18 +76,58 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
if (bestMatch is null) if (bestMatch is null)
{ {
_logger.LogDebug("No fuzzy match above threshold for: {CompanyName}", companyName); _logger.LogDebug("No fuzzy match above threshold for: {CompanyName}", companyName);
return CreateUnverifiedResult(companyName, startDate, endDate, return CreateUnverifiedResult(companyName, startDate, endDate, jobTitle,
$"No company name matched above {FuzzyMatchThreshold}% threshold"); $"No company name matched above {FuzzyMatchThreshold}% threshold");
} }
// Cache the matched company
var match = bestMatch.Value; var match = bestMatch.Value;
await CacheCompanyAsync(match.Item);
// Fetch full company details for additional data
var companyDetails = await _companiesHouseClient.GetCompanyAsync(match.Item.CompanyNumber);
// Cache the matched company with full details
await CacheCompanyAsync(match.Item, companyDetails);
_logger.LogInformation( _logger.LogInformation(
"Verified company {ClaimedName} matched to {MatchedName} with score {Score}%", "Verified company {ClaimedName} matched to {MatchedName} with score {Score}%",
companyName, match.Item.Title, match.Score); companyName, match.Item.Title, match.Score);
// Run all verification checks
var incorporationDate = DateHelpers.ParseDate(match.Item.DateOfCreation);
var dissolutionDate = DateHelpers.ParseDate(match.Item.DateOfCessation);
var companyStatus = match.Item.CompanyStatus;
var companyType = match.Item.CompanyType;
var sicCodes = companyDetails?.SicCodes ?? match.Item.SicCodes;
var accountsCategory = companyDetails?.Accounts?.LastAccounts?.Type;
// Check 1: Employment before company incorporation
CheckIncorporationDate(flags, startDate, incorporationDate, match.Item.Title);
// Check 2: Employment at dissolved company
CheckDissolutionDate(flags, endDate, dissolutionDate, companyStatus, match.Item.Title);
// Check 3: Dormant company check
CheckDormantCompany(flags, accountsCategory, jobTitle, match.Item.Title);
// Check 4: Company size vs job title
CheckCompanySizeVsRole(flags, accountsCategory, jobTitle, match.Item.Title);
// Check 5: SIC code vs job title mismatch
CheckSicCodeMismatch(flags, sicCodes, jobTitle, match.Item.Title);
// Check 6: Job title plausibility for PLCs
var (jobPlausible, jobNotes) = CheckJobTitlePlausibility(jobTitle, companyType);
if (jobPlausible == false)
{
flags.Add(new CompanyVerificationFlag
{
Type = "ImplausibleJobTitle",
Severity = "Critical",
Message = jobNotes ?? "Job title requires verification",
ScoreImpact = -15
});
}
return new CompanyVerificationResult return new CompanyVerificationResult
{ {
ClaimedCompany = companyName, ClaimedCompany = companyName,
@@ -85,13 +137,23 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
IsVerified = true, IsVerified = true,
VerificationNotes = $"Matched with {match.Score}% confidence", VerificationNotes = $"Matched with {match.Score}% confidence",
ClaimedStartDate = startDate, ClaimedStartDate = startDate,
ClaimedEndDate = endDate ClaimedEndDate = endDate,
CompanyType = companyType,
CompanyStatus = companyStatus,
IncorporationDate = incorporationDate,
DissolutionDate = dissolutionDate,
AccountsCategory = accountsCategory,
SicCodes = sicCodes,
ClaimedJobTitle = jobTitle,
JobTitlePlausible = jobPlausible,
JobTitleNotes = jobNotes,
Flags = flags
}; };
} }
catch (CompaniesHouseRateLimitException ex) catch (CompaniesHouseRateLimitException ex)
{ {
_logger.LogWarning(ex, "Rate limit hit while verifying company: {CompanyName}", companyName); _logger.LogWarning(ex, "Rate limit hit while verifying company: {CompanyName}", companyName);
return CreateUnverifiedResult(companyName, startDate, endDate, return CreateUnverifiedResult(companyName, startDate, endDate, jobTitle,
"Verification temporarily unavailable due to rate limiting"); "Verification temporarily unavailable due to rate limiting");
} }
} }
@@ -119,14 +181,364 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
}).ToList(); }).ToList();
} }
public async Task<bool?> VerifyDirectorAsync(
string companyNumber,
string candidateName,
DateOnly? startDate,
DateOnly? endDate)
{
if (string.IsNullOrWhiteSpace(companyNumber) || string.IsNullOrWhiteSpace(candidateName))
{
return null;
}
try
{
var officers = await _companiesHouseClient.GetOfficersAsync(companyNumber);
if (officers?.Items is null || officers.Items.Count == 0)
{
_logger.LogDebug("No officers found for company {CompanyNumber}", companyNumber);
return null;
}
// Normalize candidate name for comparison
var normalizedCandidate = NormalizeName(candidateName);
foreach (var officer in officers.Items)
{
// Check if officer role is director-like
var role = officer.OfficerRole?.ToLowerInvariant() ?? "";
if (!role.Contains("director") && !role.Contains("secretary"))
{
continue;
}
// Fuzzy match the name
var normalizedOfficer = NormalizeName(officer.Name);
var matchScore = Fuzz.Ratio(normalizedCandidate, normalizedOfficer);
if (matchScore >= 80) // High threshold for name matching
{
// Check date overlap
var appointedOn = DateHelpers.ParseDate(officer.AppointedOn);
var resignedOn = DateHelpers.ParseDate(officer.ResignedOn);
// If no claimed dates, just check if names match
if (!startDate.HasValue && !endDate.HasValue)
{
_logger.LogDebug(
"Found matching director {OfficerName} for candidate {CandidateName} at company {CompanyNumber}",
officer.Name, candidateName, companyNumber);
return true;
}
// Check if employment period overlaps with directorship
var datesOverlap = DatesOverlap(
startDate, endDate,
appointedOn, resignedOn);
if (datesOverlap)
{
_logger.LogDebug(
"Verified director {OfficerName} matches candidate {CandidateName} with overlapping dates",
officer.Name, candidateName);
return true;
}
}
}
_logger.LogDebug(
"No matching director found for candidate {CandidateName} at company {CompanyNumber}",
candidateName, companyNumber);
return false;
}
catch (CompaniesHouseRateLimitException)
{
_logger.LogWarning("Rate limit hit while verifying director for company {CompanyNumber}", companyNumber);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error verifying director for company {CompanyNumber}", companyNumber);
return null;
}
}
private static string NormalizeName(string name)
{
if (string.IsNullOrWhiteSpace(name)) return "";
// Companies House often stores names as "SURNAME, Firstname"
// Convert to "Firstname Surname" format for comparison
var normalized = name.ToUpperInvariant().Trim();
if (normalized.Contains(','))
{
var parts = normalized.Split(',', 2);
if (parts.Length == 2)
{
normalized = $"{parts[1].Trim()} {parts[0].Trim()}";
}
}
return normalized;
}
private static bool DatesOverlap(DateOnly? start1, DateOnly? end1, DateOnly? start2, DateOnly? end2)
{
// If no dates, assume overlap
if (!start1.HasValue && !end1.HasValue) return true;
if (!start2.HasValue && !end2.HasValue) return true;
// Use default dates for missing values
var s1 = start1 ?? DateOnly.MinValue;
var e1 = end1 ?? DateOnly.MaxValue;
var s2 = start2 ?? DateOnly.MinValue;
var e2 = end2 ?? DateOnly.MaxValue;
// Check overlap: periods overlap if one starts before the other ends
return s1 <= e2 && s2 <= e1;
}
#region Verification Checks
private static void CheckIncorporationDate(
List<CompanyVerificationFlag> flags,
DateOnly? claimedStartDate,
DateOnly? incorporationDate,
string companyName)
{
if (claimedStartDate.HasValue && incorporationDate.HasValue)
{
if (claimedStartDate.Value < incorporationDate.Value)
{
flags.Add(new CompanyVerificationFlag
{
Type = "EmploymentBeforeIncorporation",
Severity = "Critical",
Message = $"Claimed employment at '{companyName}' starting {claimedStartDate:MMM yyyy} is before company incorporation date {incorporationDate:MMM yyyy}",
ScoreImpact = -20
});
}
}
}
private static void CheckDissolutionDate(
List<CompanyVerificationFlag> flags,
DateOnly? claimedEndDate,
DateOnly? dissolutionDate,
string? companyStatus,
string companyName)
{
var isDissolvedStatus = companyStatus?.ToLowerInvariant() is "dissolved" or "liquidation" or "administration";
if (dissolutionDate.HasValue && isDissolvedStatus)
{
// Allow 3 month buffer for wind-down
var bufferDate = dissolutionDate.Value.AddMonths(3);
if (claimedEndDate.HasValue && claimedEndDate.Value > bufferDate)
{
flags.Add(new CompanyVerificationFlag
{
Type = "EmploymentAtDissolvedCompany",
Severity = "Critical",
Message = $"Claimed employment at '{companyName}' until {claimedEndDate:MMM yyyy} but company was dissolved on {dissolutionDate:MMM yyyy}",
ScoreImpact = -20
});
}
else if (!claimedEndDate.HasValue) // Current employment at dissolved company
{
flags.Add(new CompanyVerificationFlag
{
Type = "CurrentEmploymentAtDissolvedCompany",
Severity = "Critical",
Message = $"Claims current employment at '{companyName}' but company was dissolved on {dissolutionDate:MMM yyyy}",
ScoreImpact = -25
});
}
}
}
private static void CheckDormantCompany(
List<CompanyVerificationFlag> flags,
string? accountsCategory,
string? jobTitle,
string companyName)
{
if (string.IsNullOrWhiteSpace(accountsCategory)) return;
var isDormant = accountsCategory.ToLowerInvariant().Contains("dormant");
if (!isDormant) return;
// Directors can maintain dormant companies, but other roles are suspicious
var title = jobTitle?.ToLowerInvariant() ?? "";
var isDirectorRole = title.Contains("director") || title.Contains("company secretary");
if (!isDirectorRole)
{
flags.Add(new CompanyVerificationFlag
{
Type = "EmploymentAtDormantCompany",
Severity = "Warning",
Message = $"Claimed active employment as '{jobTitle}' at '{companyName}' which files dormant accounts",
ScoreImpact = -10
});
}
}
private static void CheckCompanySizeVsRole(
List<CompanyVerificationFlag> flags,
string? accountsCategory,
string? jobTitle,
string companyName)
{
if (string.IsNullOrWhiteSpace(accountsCategory) || string.IsNullOrWhiteSpace(jobTitle)) return;
var category = accountsCategory.ToLowerInvariant();
var title = jobTitle.ToLowerInvariant();
// Micro-entity: < 10 employees, < £632k turnover
var isMicroEntity = category.Contains("micro");
// Check for senior management roles at micro companies
var isSeniorRole = title.Contains("vp") ||
title.Contains("vice president") ||
title.Contains("head of") ||
title.Contains("chief") ||
title.Contains("director of") ||
title.Contains("senior director");
// At micro companies, having many senior roles is suspicious
if (isMicroEntity && isSeniorRole)
{
flags.Add(new CompanyVerificationFlag
{
Type = "SeniorRoleAtMicroCompany",
Severity = "Warning",
Message = $"Claimed senior role '{jobTitle}' at '{companyName}' which files micro-entity accounts (typically <10 employees)",
ScoreImpact = -10
});
}
}
private static void CheckSicCodeMismatch(
List<CompanyVerificationFlag> flags,
List<string>? sicCodes,
string? jobTitle,
string companyName)
{
if (sicCodes is null || sicCodes.Count == 0 || string.IsNullOrWhiteSpace(jobTitle)) return;
var title = jobTitle.ToLowerInvariant();
// Check if this is a tech role
var isTechRole = title.Contains("software") ||
title.Contains("developer") ||
title.Contains("engineer") ||
title.Contains("programmer") ||
title.Contains("data scientist") ||
title.Contains("data analyst") ||
title.Contains("devops") ||
title.Contains("cloud") ||
title.Contains("machine learning") ||
title.Contains("ai ") ||
title.Contains("frontend") ||
title.Contains("backend") ||
title.Contains("full stack") ||
title.Contains("fullstack");
if (isTechRole)
{
// Check if company has any tech SIC codes
var hasTechSic = sicCodes.Any(s => TechSicCodes.Contains(s));
if (!hasTechSic)
{
// Get the primary SIC code description (simplified - just show code)
var primarySic = sicCodes.FirstOrDefault() ?? "Unknown";
flags.Add(new CompanyVerificationFlag
{
Type = "SicCodeMismatch",
Severity = "Info",
Message = $"Tech role '{jobTitle}' at '{companyName}' (SIC: {primarySic}) - company is not registered as a technology business",
ScoreImpact = -5
});
}
}
}
private static (bool? IsPlausible, string? Notes) CheckJobTitlePlausibility(string? jobTitle, string? companyType)
{
if (string.IsNullOrWhiteSpace(jobTitle) || string.IsNullOrWhiteSpace(companyType))
{
return (null, null);
}
var title = jobTitle.Trim().ToLowerInvariant();
var type = companyType.Trim().ToLowerInvariant();
// Check if this is a PLC (Public Limited Company) - these are large companies
var isPlc = type.Contains("plc") || type.Contains("public limited");
// Check for C-suite / very senior roles
var isCsuiteRole = title.Contains("ceo") ||
title.Contains("chief executive") ||
title.Contains("cto") ||
title.Contains("chief technology") ||
title.Contains("cfo") ||
title.Contains("chief financial") ||
title.Contains("coo") ||
title.Contains("chief operating") ||
title.Contains("cio") ||
title.Contains("chief information") ||
title.Contains("managing director") ||
title == "md" ||
title.Contains("chairman") ||
title.Contains("chairwoman") ||
title.Contains("chairperson") ||
title.Contains("president");
// Check for board-level roles
var isBoardRole = title.Contains("board member") ||
title.Contains("non-executive director") ||
title.Contains("executive director") ||
(title == "director" && !title.Contains("of"));
if (isPlc && (isCsuiteRole || isBoardRole))
{
return (false, $"Claimed senior role '{jobTitle}' at a PLC requires verification - C-suite positions at public companies are publicly disclosed");
}
// Check for VP/SVP at PLCs (also usually disclosed)
var isVpRole = title.Contains("vice president") ||
title.Contains("vp ") ||
title.StartsWith("vp") ||
title.Contains("svp") ||
title.Contains("senior vice president") ||
title.Contains("evp") ||
title.Contains("executive vice president");
if (isPlc && isVpRole)
{
return (false, $"Claimed VP-level role '{jobTitle}' at a PLC - senior positions at public companies should be verifiable");
}
return (true, null);
}
#endregion
#region Helper Methods
private async Task<CompanyCache?> FindCachedMatchAsync(string companyName) private async Task<CompanyCache?> FindCachedMatchAsync(string companyName)
{ {
var cutoffDate = DateTime.UtcNow.AddDays(-CacheExpirationDays); var cutoffDate = DateTime.UtcNow.AddDays(-CacheExpirationDays);
// Use factory to create a new DbContext for thread-safe parallel access
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
// Get recent cached companies
var cachedCompanies = await dbContext.CompanyCache var cachedCompanies = await dbContext.CompanyCache
.Where(c => c.CachedAt >= cutoffDate) .Where(c => c.CachedAt >= cutoffDate)
.ToListAsync(); .ToListAsync();
@@ -136,7 +548,6 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
return null; return null;
} }
// Find best fuzzy match in cache
var matches = cachedCompanies var matches = cachedCompanies
.Select(c => new { Company = c, Score = Fuzz.Ratio(companyName.ToUpperInvariant(), c.CompanyName.ToUpperInvariant()) }) .Select(c => new { Company = c, Score = Fuzz.Ratio(companyName.ToUpperInvariant(), c.CompanyName.ToUpperInvariant()) })
.Where(m => m.Score >= FuzzyMatchThreshold) .Where(m => m.Score >= FuzzyMatchThreshold)
@@ -161,20 +572,26 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
return matches.Count > 0 ? matches[0] : null; return matches.Count > 0 ? matches[0] : null;
} }
private async Task CacheCompanyAsync(CompaniesHouseSearchItem item) private async Task CacheCompanyAsync(CompaniesHouseSearchItem item, CompaniesHouseCompany? details)
{ {
// Use factory to create a new DbContext for thread-safe parallel access
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
var existingCache = await dbContext.CompanyCache var existingCache = await dbContext.CompanyCache
.FirstOrDefaultAsync(c => c.CompanyNumber == item.CompanyNumber); .FirstOrDefaultAsync(c => c.CompanyNumber == item.CompanyNumber);
var sicCodes = details?.SicCodes ?? item.SicCodes;
var sicCodesJson = sicCodes != null ? JsonSerializer.Serialize(sicCodes) : null;
var accountsCategory = details?.Accounts?.LastAccounts?.Type;
if (existingCache is not null) if (existingCache is not null)
{ {
existingCache.CompanyName = item.Title; existingCache.CompanyName = item.Title;
existingCache.Status = item.CompanyStatus ?? "Unknown"; existingCache.Status = item.CompanyStatus ?? "Unknown";
existingCache.CompanyType = item.CompanyType;
existingCache.IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation); existingCache.IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation);
existingCache.DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation); existingCache.DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation);
existingCache.AccountsCategory = accountsCategory;
existingCache.SicCodesJson = sicCodesJson;
existingCache.CachedAt = DateTime.UtcNow; existingCache.CachedAt = DateTime.UtcNow;
} }
else else
@@ -184,8 +601,11 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
CompanyNumber = item.CompanyNumber, CompanyNumber = item.CompanyNumber,
CompanyName = item.Title, CompanyName = item.Title,
Status = item.CompanyStatus ?? "Unknown", Status = item.CompanyStatus ?? "Unknown",
CompanyType = item.CompanyType,
IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation), IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation),
DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation), DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation),
AccountsCategory = accountsCategory,
SicCodesJson = sicCodesJson,
CachedAt = DateTime.UtcNow CachedAt = DateTime.UtcNow
}; };
@@ -195,16 +615,41 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
} }
private static CompanyVerificationResult CreateVerificationResult( private CompanyVerificationResult CreateResultFromCache(
string claimedCompany,
CompanyCache cached, CompanyCache cached,
string claimedCompany,
DateOnly? startDate, DateOnly? startDate,
DateOnly? endDate) DateOnly? endDate,
string? jobTitle,
List<CompanyVerificationFlag> flags)
{ {
var matchScore = Fuzz.Ratio( var matchScore = Fuzz.Ratio(
claimedCompany.ToUpperInvariant(), claimedCompany.ToUpperInvariant(),
cached.CompanyName.ToUpperInvariant()); cached.CompanyName.ToUpperInvariant());
var sicCodes = !string.IsNullOrEmpty(cached.SicCodesJson)
? JsonSerializer.Deserialize<List<string>>(cached.SicCodesJson)
: null;
// Run all verification checks
CheckIncorporationDate(flags, startDate, cached.IncorporationDate, cached.CompanyName);
CheckDissolutionDate(flags, endDate, cached.DissolutionDate, cached.Status, cached.CompanyName);
CheckDormantCompany(flags, cached.AccountsCategory, jobTitle, cached.CompanyName);
CheckCompanySizeVsRole(flags, cached.AccountsCategory, jobTitle, cached.CompanyName);
CheckSicCodeMismatch(flags, sicCodes, jobTitle, cached.CompanyName);
var (jobPlausible, jobNotes) = CheckJobTitlePlausibility(jobTitle, cached.CompanyType);
if (jobPlausible == false)
{
flags.Add(new CompanyVerificationFlag
{
Type = "ImplausibleJobTitle",
Severity = "Critical",
Message = jobNotes ?? "Job title requires verification",
ScoreImpact = -15
});
}
return new CompanyVerificationResult return new CompanyVerificationResult
{ {
ClaimedCompany = claimedCompany, ClaimedCompany = claimedCompany,
@@ -214,7 +659,17 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
IsVerified = true, IsVerified = true,
VerificationNotes = $"Matched from cache with {matchScore}% confidence", VerificationNotes = $"Matched from cache with {matchScore}% confidence",
ClaimedStartDate = startDate, ClaimedStartDate = startDate,
ClaimedEndDate = endDate ClaimedEndDate = endDate,
CompanyType = cached.CompanyType,
CompanyStatus = cached.Status,
IncorporationDate = cached.IncorporationDate,
DissolutionDate = cached.DissolutionDate,
AccountsCategory = cached.AccountsCategory,
SicCodes = sicCodes,
ClaimedJobTitle = jobTitle,
JobTitlePlausible = jobPlausible,
JobTitleNotes = jobNotes,
Flags = flags
}; };
} }
@@ -222,6 +677,7 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
string companyName, string companyName,
DateOnly? startDate, DateOnly? startDate,
DateOnly? endDate, DateOnly? endDate,
string? jobTitle,
string reason) string reason)
{ {
return new CompanyVerificationResult return new CompanyVerificationResult
@@ -233,8 +689,10 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
IsVerified = false, IsVerified = false,
VerificationNotes = reason, VerificationNotes = reason,
ClaimedStartDate = startDate, ClaimedStartDate = startDate,
ClaimedEndDate = endDate ClaimedEndDate = endDate,
ClaimedJobTitle = jobTitle
}; };
} }
#endregion
} }

View File

@@ -1,6 +1,7 @@
@page "/dashboard" @page "/dashboard"
@attribute [Authorize] @attribute [Authorize]
@rendermode InteractiveServer @rendermode InteractiveServer
@implements IDisposable
@inject ICVCheckService CVCheckService @inject ICVCheckService CVCheckService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@@ -250,10 +251,54 @@
private bool _isExporting; private bool _isExporting;
private string? _errorMessage; private string? _errorMessage;
private Guid _userId; private Guid _userId;
private System.Threading.Timer? _pollingTimer;
private bool _isPolling;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await LoadChecks(); await LoadChecks();
StartPollingIfNeeded();
}
private void StartPollingIfNeeded()
{
if (HasProcessingChecks() && !_isPolling)
{
_isPolling = true;
_pollingTimer = new System.Threading.Timer(async _ =>
{
await InvokeAsync(async () =>
{
await LoadChecks();
if (!HasProcessingChecks())
{
StopPolling();
}
StateHasChanged();
});
}, null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3));
}
}
private bool HasProcessingChecks()
{
foreach (var c in _checks)
{
if (c.Status == "Processing" || c.Status == "Pending") return true;
}
return false;
}
private void StopPolling()
{
_isPolling = false;
_pollingTimer?.Dispose();
_pollingTimer = null;
}
public void Dispose()
{
StopPolling();
} }
private async Task LoadChecks() private async Task LoadChecks()

View File

@@ -115,7 +115,7 @@
<div class="card-body p-4"> <div class="card-body p-4">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-md-4 text-center border-end"> <div class="col-md-4 text-center border-end">
<div class="score-circle @GetScoreColorClass(_report.OverallScore) mx-auto mb-2"> <div class="score-circle @GetScoreColorClass(_report!.OverallScore) mx-auto mb-2">
<span class="score-value">@_report.OverallScore</span> <span class="score-value">@_report.OverallScore</span>
</div> </div>
<h5 class="mb-0">@_report.ScoreLabel</h5> <h5 class="mb-0">@_report.ScoreLabel</h5>

View File

@@ -194,7 +194,8 @@ public sealed class ProcessCVCheckJobTests : IDisposable
x => x.VerifyCompanyAsync( x => x.VerifyCompanyAsync(
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<DateOnly?>(), It.IsAny<DateOnly?>(),
It.IsAny<DateOnly?>()), It.IsAny<DateOnly?>(),
It.IsAny<string?>()),
Times.Exactly(3)); Times.Exactly(3));
} }
@@ -1031,7 +1032,8 @@ public sealed class ProcessCVCheckJobTests : IDisposable
.Setup(x => x.VerifyCompanyAsync( .Setup(x => x.VerifyCompanyAsync(
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<DateOnly?>(), It.IsAny<DateOnly?>(),
It.IsAny<DateOnly?>())) It.IsAny<DateOnly?>(),
It.IsAny<string?>()))
.ReturnsAsync(() => queue.Count > 0 ? queue.Dequeue() : CreateDefaultVerificationResult()); .ReturnsAsync(() => queue.Count > 0 ? queue.Dequeue() : CreateDefaultVerificationResult());
} }
else else
@@ -1040,7 +1042,8 @@ public sealed class ProcessCVCheckJobTests : IDisposable
.Setup(x => x.VerifyCompanyAsync( .Setup(x => x.VerifyCompanyAsync(
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<DateOnly?>(), It.IsAny<DateOnly?>(),
It.IsAny<DateOnly?>())) It.IsAny<DateOnly?>(),
It.IsAny<string?>()))
.ReturnsAsync(CreateDefaultVerificationResult()); .ReturnsAsync(CreateDefaultVerificationResult());
} }