Fix file upload stale reference and improve company matching

File upload fix:
- Buffer file data immediately on selection to prevent stale IBrowserFile references
- Add BufferedFile record to hold file data in memory
- Add loading indicator while files are being buffered
- Fixes "Cannot read properties of null (reading '_blazorFilesById')" error

Company matching improvement:
- Prefer companies that existed at the claimed employment start date
- Fixes matching wrong company when newer company has similar name
- Example: "UK MATTEL LTD" (2025) vs "MATTEL U.K. LIMITED" (1980)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 21:14:01 +01:00
parent 3c8ee0a529
commit 67a98405af
2 changed files with 88 additions and 27 deletions

View File

@@ -70,8 +70,8 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
return CreateUnverifiedResult(companyName, startDate, endDate, jobTitle, "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, preferring companies that existed at claimed start date
var bestMatch = FindBestMatch(companyName, searchResponse.Items); var bestMatch = FindBestMatch(companyName, searchResponse.Items, startDate);
if (bestMatch is null) if (bestMatch is null)
{ {
@@ -560,7 +560,8 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
private static (CompaniesHouseSearchItem Item, int Score)? FindBestMatch( private static (CompaniesHouseSearchItem Item, int Score)? FindBestMatch(
string companyName, string companyName,
List<CompaniesHouseSearchItem> items) List<CompaniesHouseSearchItem> items,
DateOnly? claimedStartDate)
{ {
var normalizedSearch = companyName.ToUpperInvariant(); var normalizedSearch = companyName.ToUpperInvariant();
@@ -568,10 +569,32 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
.Where(item => !string.IsNullOrWhiteSpace(item.Title)) .Where(item => !string.IsNullOrWhiteSpace(item.Title))
.Select(item => (Item: item, Score: Fuzz.TokenSetRatio(normalizedSearch, item.Title.ToUpperInvariant()))) .Select(item => (Item: item, Score: Fuzz.TokenSetRatio(normalizedSearch, item.Title.ToUpperInvariant())))
.Where(m => m.Score >= FuzzyMatchThreshold) .Where(m => m.Score >= FuzzyMatchThreshold)
.ToList();
if (matches.Count == 0) return null;
// If we have a claimed start date, prefer companies that existed at that time
if (claimedStartDate.HasValue)
{
var existedAtStartDate = matches
.Where(m =>
{
var incDate = DateHelpers.ParseDate(m.Item.DateOfCreation);
// Company existed if it was incorporated before the claimed start date
return incDate == null || incDate <= claimedStartDate.Value;
})
.OrderByDescending(m => m.Score) .OrderByDescending(m => m.Score)
.ToList(); .ToList();
return matches.Count > 0 ? matches[0] : null; // If any matches existed at the start date, prefer those
if (existedAtStartDate.Count > 0)
{
return existedAtStartDate[0];
}
}
// Fall back to highest score if no temporal match
return matches.OrderByDescending(m => m.Score).First();
} }
private async Task CacheCompanyAsync(CompaniesHouseSearchItem item, CompaniesHouseCompany? details) private async Task CacheCompanyAsync(CompaniesHouseSearchItem item, CompaniesHouseCompany? details)

View File

@@ -76,7 +76,17 @@
</label> </label>
</div> </div>
@if (_selectedFiles.Count > 0) @if (_isBuffering)
{
<div class="mt-4 text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading files...</span>
</div>
<p class="mt-2 text-muted">Reading files...</p>
</div>
}
@if (_selectedFiles.Count > 0 && !_isBuffering)
{ {
<div class="mt-4"> <div class="mt-4">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
@@ -156,8 +166,9 @@
</style> </style>
@code { @code {
private List<IBrowserFile> _selectedFiles = new(); private List<BufferedFile> _selectedFiles = new();
private bool _isUploading; private bool _isUploading;
private bool _isBuffering;
private bool _isDragging; private bool _isDragging;
private int _uploadProgress; private int _uploadProgress;
private string? _errorMessage; private string? _errorMessage;
@@ -172,6 +183,9 @@
private static readonly byte[] PdfMagicBytes = [0x25, 0x50, 0x44, 0x46]; // %PDF private static readonly byte[] PdfMagicBytes = [0x25, 0x50, 0x44, 0x46]; // %PDF
private static readonly byte[] DocxMagicBytes = [0x50, 0x4B, 0x03, 0x04]; // PK.. (ZIP signature) private static readonly byte[] DocxMagicBytes = [0x50, 0x4B, 0x03, 0x04]; // PK.. (ZIP signature)
// Buffered file to prevent stale IBrowserFile references
private sealed record BufferedFile(string Name, long Size, byte[] Data);
private void HandleDragEnter() private void HandleDragEnter()
{ {
_isDragging = true; _isDragging = true;
@@ -187,12 +201,18 @@
_isDragging = false; _isDragging = false;
} }
private void HandleFileSelected(InputFileChangeEventArgs e) private async Task HandleFileSelected(InputFileChangeEventArgs e)
{ {
_errorMessage = null; _errorMessage = null;
_isBuffering = true;
StateHasChanged();
var invalidFiles = new List<string>(); var invalidFiles = new List<string>();
var oversizedFiles = new List<string>(); var oversizedFiles = new List<string>();
var failedFiles = new List<string>();
try
{
foreach (var file in e.GetMultipleFiles(MaxFileCount)) foreach (var file in e.GetMultipleFiles(MaxFileCount))
{ {
if (!IsValidFileType(file.Name)) if (!IsValidFileType(file.Name))
@@ -208,10 +228,29 @@
} }
// Avoid duplicates // Avoid duplicates
if (!_selectedFiles.Any(f => f.Name == file.Name && f.Size == file.Size)) if (_selectedFiles.Any(f => f.Name == file.Name && f.Size == file.Size))
{ {
_selectedFiles.Add(file); continue;
} }
// Read file data immediately to prevent stale reference issues
try
{
await using var stream = file.OpenReadStream(MaxFileSize);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
_selectedFiles.Add(new BufferedFile(file.Name, file.Size, memoryStream.ToArray()));
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to buffer file: {FileName}", file.Name);
failedFiles.Add(file.Name);
}
}
}
finally
{
_isBuffering = false;
} }
// Build error message if any files were rejected // Build error message if any files were rejected
@@ -220,12 +259,14 @@
errors.Add($"Invalid file type(s): {string.Join(", ", invalidFiles.Take(3))}{(invalidFiles.Count > 3 ? $" and {invalidFiles.Count - 3} more" : "")}"); errors.Add($"Invalid file type(s): {string.Join(", ", invalidFiles.Take(3))}{(invalidFiles.Count > 3 ? $" and {invalidFiles.Count - 3} more" : "")}");
if (oversizedFiles.Count > 0) if (oversizedFiles.Count > 0)
errors.Add($"File(s) exceed 10MB limit: {string.Join(", ", oversizedFiles.Take(3))}{(oversizedFiles.Count > 3 ? $" and {oversizedFiles.Count - 3} more" : "")}"); errors.Add($"File(s) exceed 10MB limit: {string.Join(", ", oversizedFiles.Take(3))}{(oversizedFiles.Count > 3 ? $" and {oversizedFiles.Count - 3} more" : "")}");
if (failedFiles.Count > 0)
errors.Add($"Failed to read file(s): {string.Join(", ", failedFiles.Take(3))}{(failedFiles.Count > 3 ? $" and {failedFiles.Count - 3} more" : "")}");
if (errors.Count > 0) if (errors.Count > 0)
_errorMessage = string.Join(". ", errors); _errorMessage = string.Join(". ", errors);
} }
private void RemoveFile(IBrowserFile file) private void RemoveFile(BufferedFile file)
{ {
_selectedFiles.Remove(file); _selectedFiles.Remove(file);
} }
@@ -269,10 +310,7 @@
try try
{ {
await using var stream = file.OpenReadStream(MaxFileSize); using var memoryStream = new MemoryStream(file.Data);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
memoryStream.Position = 0;
// Validate file content (magic bytes) // Validate file content (magic bytes)
if (!await ValidateFileContentAsync(memoryStream, file.Name)) if (!await ValidateFileContentAsync(memoryStream, file.Name))