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:
@@ -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)
|
||||||
.OrderByDescending(m => m.Score)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return matches.Count > 0 ? matches[0] : null;
|
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)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|||||||
@@ -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,32 +201,57 @@
|
|||||||
_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>();
|
||||||
|
|
||||||
foreach (var file in e.GetMultipleFiles(MaxFileCount))
|
try
|
||||||
{
|
{
|
||||||
if (!IsValidFileType(file.Name))
|
foreach (var file in e.GetMultipleFiles(MaxFileCount))
|
||||||
{
|
{
|
||||||
invalidFiles.Add(file.Name);
|
if (!IsValidFileType(file.Name))
|
||||||
continue;
|
{
|
||||||
}
|
invalidFiles.Add(file.Name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (file.Size > MaxFileSize)
|
if (file.Size > MaxFileSize)
|
||||||
{
|
{
|
||||||
oversizedFiles.Add(file.Name);
|
oversizedFiles.Add(file.Name);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
var errors = new List<string>();
|
var errors = new List<string>();
|
||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user