Add audit logging, processing stages, delete functionality, and bug fixes
- Add audit logging system for tracking CV uploads, processing, deletion, report views, and PDF exports for billing/reference purposes - Add processing stage display on dashboard instead of generic "Processing" - Add delete button for CV checks on dashboard - Fix duplicate primary key error in CompanyCache (race condition) - Fix DbContext concurrency in Dashboard (concurrent delete/load operations) - Fix ProcessCVCheckJob to handle deleted records gracefully - Fix duplicate flags in verification report by deduplicating on Title+Description - Remove internal cache notes from verification results - Add EF migrations for ProcessingStage and AuditLog table Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ public sealed class ProcessCVCheckJob
|
||||
private readonly ICompanyVerifierService _companyVerifierService;
|
||||
private readonly IEducationVerifierService _educationVerifierService;
|
||||
private readonly ITimelineAnalyserService _timelineAnalyserService;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly ILogger<ProcessCVCheckJob> _logger;
|
||||
|
||||
private const int BaseScore = 100;
|
||||
@@ -41,6 +42,7 @@ public sealed class ProcessCVCheckJob
|
||||
ICompanyVerifierService companyVerifierService,
|
||||
IEducationVerifierService educationVerifierService,
|
||||
ITimelineAnalyserService timelineAnalyserService,
|
||||
IAuditService auditService,
|
||||
ILogger<ProcessCVCheckJob> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
@@ -49,6 +51,7 @@ public sealed class ProcessCVCheckJob
|
||||
_companyVerifierService = companyVerifierService;
|
||||
_educationVerifierService = educationVerifierService;
|
||||
_timelineAnalyserService = timelineAnalyserService;
|
||||
_auditService = auditService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -69,6 +72,7 @@ public sealed class ProcessCVCheckJob
|
||||
{
|
||||
// Step 1: Update status to Processing
|
||||
cvCheck.Status = CheckStatus.Processing;
|
||||
cvCheck.ProcessingStage = "Downloading CV";
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogDebug("CV check {CheckId} status updated to Processing", cvCheckId);
|
||||
@@ -79,6 +83,9 @@ public sealed class ProcessCVCheckJob
|
||||
_logger.LogDebug("Downloaded CV file for check {CheckId}", cvCheckId);
|
||||
|
||||
// Step 3: Parse CV
|
||||
cvCheck.ProcessingStage = "Parsing CV";
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var cvData = await _cvParserService.ParseAsync(fileStream, cvCheck.OriginalFileName, cancellationToken);
|
||||
|
||||
_logger.LogDebug(
|
||||
@@ -87,6 +94,7 @@ public sealed class ProcessCVCheckJob
|
||||
|
||||
// Step 4: Save extracted data
|
||||
cvCheck.ExtractedDataJson = JsonSerializer.Serialize(cvData, JsonDefaults.CamelCaseIndented);
|
||||
cvCheck.ProcessingStage = "Verifying Employment";
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Step 5: Verify each employment entry (parallelized with rate limiting)
|
||||
@@ -128,9 +136,14 @@ public sealed class ProcessCVCheckJob
|
||||
}
|
||||
|
||||
// Step 5b: Verify director claims against Companies House officers
|
||||
cvCheck.ProcessingStage = "Verifying Directors";
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await VerifyDirectorClaims(cvData.FullName, verificationResults, cancellationToken);
|
||||
|
||||
// Step 6: Verify education entries
|
||||
cvCheck.ProcessingStage = "Verifying Education";
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
var educationResults = _educationVerifierService.VerifyAll(
|
||||
cvData.Education,
|
||||
cvData.Employment);
|
||||
@@ -143,6 +156,9 @@ public sealed class ProcessCVCheckJob
|
||||
educationResults.Count(e => e.IsDiplomaMill));
|
||||
|
||||
// Step 7: Analyse timeline
|
||||
cvCheck.ProcessingStage = "Analyzing Timeline";
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var timelineAnalysis = _timelineAnalyserService.Analyse(cvData.Employment);
|
||||
|
||||
_logger.LogDebug(
|
||||
@@ -150,6 +166,8 @@ public sealed class ProcessCVCheckJob
|
||||
cvCheckId, timelineAnalysis.Gaps.Count, timelineAnalysis.Overlaps.Count);
|
||||
|
||||
// Step 8: Calculate veracity score
|
||||
cvCheck.ProcessingStage = "Calculating Score";
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
var (score, flags) = CalculateVeracityScore(verificationResults, educationResults, timelineAnalysis, cvData);
|
||||
|
||||
_logger.LogDebug("Calculated veracity score for check {CheckId}: {Score}", cvCheckId, score);
|
||||
@@ -184,6 +202,9 @@ public sealed class ProcessCVCheckJob
|
||||
}
|
||||
|
||||
// Step 10: Generate veracity report
|
||||
cvCheck.ProcessingStage = "Generating Report";
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var report = new VeracityReport
|
||||
{
|
||||
OverallScore = score,
|
||||
@@ -200,20 +221,32 @@ public sealed class ProcessCVCheckJob
|
||||
|
||||
// Step 11: Update status to Completed
|
||||
cvCheck.Status = CheckStatus.Completed;
|
||||
cvCheck.ProcessingStage = null; // Clear stage on completion
|
||||
cvCheck.CompletedAt = DateTime.UtcNow;
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"CV check {CheckId} completed successfully with score {Score}",
|
||||
cvCheckId, score);
|
||||
|
||||
await _auditService.LogAsync(cvCheck.UserId, AuditActions.CVProcessed, "CVCheck", cvCheckId, $"Score: {score}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing CV check {CheckId}", cvCheckId);
|
||||
|
||||
cvCheck.Status = CheckStatus.Failed;
|
||||
// Use CancellationToken.None to ensure failure status is saved even if original token is cancelled
|
||||
await _dbContext.SaveChangesAsync(CancellationToken.None);
|
||||
try
|
||||
{
|
||||
cvCheck.Status = CheckStatus.Failed;
|
||||
// Use CancellationToken.None to ensure failure status is saved even if original token is cancelled
|
||||
await _dbContext.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
// Record was deleted during processing - nothing to update
|
||||
_logger.LogWarning("CV check {CheckId} was deleted during processing", cvCheckId);
|
||||
return;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
@@ -382,10 +415,19 @@ public sealed class ProcessCVCheckJob
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure score doesn't go below 0
|
||||
score = Math.Max(0, score);
|
||||
// Deduplicate flags based on Title + Description
|
||||
var uniqueFlags = flags
|
||||
.GroupBy(f => (f.Title, f.Description))
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
|
||||
return (score, flags);
|
||||
// Recalculate score based on unique flags
|
||||
var uniqueScore = BaseScore + uniqueFlags.Sum(f => f.ScoreImpact);
|
||||
|
||||
// Ensure score doesn't go below 0
|
||||
uniqueScore = Math.Max(0, uniqueScore);
|
||||
|
||||
return (uniqueScore, uniqueFlags);
|
||||
}
|
||||
|
||||
private static string GetScoreLabel(int score)
|
||||
@@ -420,8 +462,13 @@ public sealed class ProcessCVCheckJob
|
||||
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)))
|
||||
// Find all director claims at verified companies - use ToList() to avoid modifying during enumeration
|
||||
var directorCandidates = verificationResults
|
||||
.Select((result, index) => (result, index))
|
||||
.Where(x => x.result.IsVerified && !string.IsNullOrEmpty(x.result.MatchedCompanyNumber))
|
||||
.ToList();
|
||||
|
||||
foreach (var (result, index) in directorCandidates)
|
||||
{
|
||||
var jobTitle = result.ClaimedJobTitle?.ToLowerInvariant() ?? "";
|
||||
|
||||
@@ -446,7 +493,7 @@ public sealed class ProcessCVCheckJob
|
||||
if (isVerifiedDirector == false)
|
||||
{
|
||||
// Add a flag for unverified director claim
|
||||
var flags = result.Flags.ToList();
|
||||
var flags = (result.Flags ?? []).ToList();
|
||||
flags.Add(new CompanyVerificationFlag
|
||||
{
|
||||
Type = "UnverifiedDirectorClaim",
|
||||
@@ -456,7 +503,6 @@ public sealed class ProcessCVCheckJob
|
||||
});
|
||||
|
||||
// Update the result with the new flag
|
||||
var index = verificationResults.IndexOf(result);
|
||||
verificationResults[index] = result with { Flags = flags };
|
||||
|
||||
_logger.LogWarning(
|
||||
|
||||
Reference in New Issue
Block a user