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,7 +18,7 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||
private readonly ILogger<CompanyVerifierService> _logger;
|
||||
|
||||
private const int FuzzyMatchThreshold = 70;
|
||||
private const int FuzzyMatchThreshold = 85;
|
||||
private const int CacheExpirationDays = 30;
|
||||
|
||||
// SIC codes for tech/software companies
|
||||
@@ -135,7 +135,7 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
MatchedCompanyNumber = match.Item.CompanyNumber,
|
||||
MatchScore = match.Score,
|
||||
IsVerified = true,
|
||||
VerificationNotes = $"Matched with {match.Score}% confidence",
|
||||
VerificationNotes = null,
|
||||
ClaimedStartDate = startDate,
|
||||
ClaimedEndDate = endDate,
|
||||
CompanyType = companyType,
|
||||
@@ -549,7 +549,8 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
}
|
||||
|
||||
var matches = cachedCompanies
|
||||
.Select(c => new { Company = c, Score = Fuzz.Ratio(companyName.ToUpperInvariant(), c.CompanyName.ToUpperInvariant()) })
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.CompanyName))
|
||||
.Select(c => new { Company = c, Score = Fuzz.TokenSetRatio(companyName.ToUpperInvariant(), c.CompanyName.ToUpperInvariant()) })
|
||||
.Where(m => m.Score >= FuzzyMatchThreshold)
|
||||
.OrderByDescending(m => m.Score)
|
||||
.FirstOrDefault();
|
||||
@@ -564,7 +565,8 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
var normalizedSearch = companyName.ToUpperInvariant();
|
||||
|
||||
var matches = items
|
||||
.Select(item => (Item: item, Score: Fuzz.Ratio(normalizedSearch, item.Title.ToUpperInvariant())))
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item.Title))
|
||||
.Select(item => (Item: item, Score: Fuzz.TokenSetRatio(normalizedSearch, item.Title.ToUpperInvariant())))
|
||||
.Where(m => m.Score >= FuzzyMatchThreshold)
|
||||
.OrderByDescending(m => m.Score)
|
||||
.ToList();
|
||||
@@ -574,45 +576,53 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
|
||||
private async Task CacheCompanyAsync(CompaniesHouseSearchItem item, CompaniesHouseCompany? details)
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var existingCache = await dbContext.CompanyCache
|
||||
.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)
|
||||
try
|
||||
{
|
||||
existingCache.CompanyName = item.Title;
|
||||
existingCache.Status = item.CompanyStatus ?? "Unknown";
|
||||
existingCache.CompanyType = item.CompanyType;
|
||||
existingCache.IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation);
|
||||
existingCache.DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation);
|
||||
existingCache.AccountsCategory = accountsCategory;
|
||||
existingCache.SicCodesJson = sicCodesJson;
|
||||
existingCache.CachedAt = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
var cacheEntry = new CompanyCache
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var existingCache = await dbContext.CompanyCache
|
||||
.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)
|
||||
{
|
||||
CompanyNumber = item.CompanyNumber,
|
||||
CompanyName = item.Title,
|
||||
Status = item.CompanyStatus ?? "Unknown",
|
||||
CompanyType = item.CompanyType,
|
||||
IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation),
|
||||
DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation),
|
||||
AccountsCategory = accountsCategory,
|
||||
SicCodesJson = sicCodesJson,
|
||||
CachedAt = DateTime.UtcNow
|
||||
};
|
||||
existingCache.CompanyName = item.Title;
|
||||
existingCache.Status = item.CompanyStatus ?? "Unknown";
|
||||
existingCache.CompanyType = item.CompanyType;
|
||||
existingCache.IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation);
|
||||
existingCache.DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation);
|
||||
existingCache.AccountsCategory = accountsCategory;
|
||||
existingCache.SicCodesJson = sicCodesJson;
|
||||
existingCache.CachedAt = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
var cacheEntry = new CompanyCache
|
||||
{
|
||||
CompanyNumber = item.CompanyNumber,
|
||||
CompanyName = item.Title,
|
||||
Status = item.CompanyStatus ?? "Unknown",
|
||||
CompanyType = item.CompanyType,
|
||||
IncorporationDate = DateHelpers.ParseDate(item.DateOfCreation),
|
||||
DissolutionDate = DateHelpers.ParseDate(item.DateOfCessation),
|
||||
AccountsCategory = accountsCategory,
|
||||
SicCodesJson = sicCodesJson,
|
||||
CachedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
dbContext.CompanyCache.Add(cacheEntry);
|
||||
dbContext.CompanyCache.Add(cacheEntry);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("PK_CompanyCache") == true)
|
||||
{
|
||||
// Race condition: another task already cached this company - ignore
|
||||
_logger.LogDebug("Company {CompanyNumber} already cached by another task", item.CompanyNumber);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private CompanyVerificationResult CreateResultFromCache(
|
||||
@@ -623,13 +633,22 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
string? jobTitle,
|
||||
List<CompanyVerificationFlag> flags)
|
||||
{
|
||||
var matchScore = Fuzz.Ratio(
|
||||
var matchScore = Fuzz.TokenSetRatio(
|
||||
claimedCompany.ToUpperInvariant(),
|
||||
cached.CompanyName.ToUpperInvariant());
|
||||
|
||||
var sicCodes = !string.IsNullOrEmpty(cached.SicCodesJson)
|
||||
? JsonSerializer.Deserialize<List<string>>(cached.SicCodesJson)
|
||||
: null;
|
||||
List<string>? sicCodes = null;
|
||||
if (!string.IsNullOrEmpty(cached.SicCodesJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
sicCodes = JsonSerializer.Deserialize<List<string>>(cached.SicCodesJson);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Ignore malformed JSON in cache
|
||||
}
|
||||
}
|
||||
|
||||
// Run all verification checks
|
||||
CheckIncorporationDate(flags, startDate, cached.IncorporationDate, cached.CompanyName);
|
||||
@@ -657,7 +676,7 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
MatchedCompanyNumber = cached.CompanyNumber,
|
||||
MatchScore = matchScore,
|
||||
IsVerified = true,
|
||||
VerificationNotes = $"Matched from cache with {matchScore}% confidence",
|
||||
VerificationNotes = null,
|
||||
ClaimedStartDate = startDate,
|
||||
ClaimedEndDate = endDate,
|
||||
CompanyType = cached.CompanyType,
|
||||
|
||||
Reference in New Issue
Block a user