From 67a98405afaaa67687014ef25bc88a3e536e4e52 Mon Sep 17 00:00:00 2001 From: peter Date: Tue, 20 Jan 2026 21:14:01 +0100 Subject: [PATCH] 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 --- .../Services/CompanyVerifierService.cs | 33 ++++++-- src/TrueCV.Web/Components/Pages/Check.razor | 82 ++++++++++++++----- 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/src/TrueCV.Infrastructure/Services/CompanyVerifierService.cs b/src/TrueCV.Infrastructure/Services/CompanyVerifierService.cs index fb3c606..782f05b 100644 --- a/src/TrueCV.Infrastructure/Services/CompanyVerifierService.cs +++ b/src/TrueCV.Infrastructure/Services/CompanyVerifierService.cs @@ -70,8 +70,8 @@ public sealed class CompanyVerifierService : ICompanyVerifierService return CreateUnverifiedResult(companyName, startDate, endDate, jobTitle, "No matching company found in Companies House"); } - // Find best fuzzy match - var bestMatch = FindBestMatch(companyName, searchResponse.Items); + // Find best fuzzy match, preferring companies that existed at claimed start date + var bestMatch = FindBestMatch(companyName, searchResponse.Items, startDate); if (bestMatch is null) { @@ -560,7 +560,8 @@ public sealed class CompanyVerifierService : ICompanyVerifierService private static (CompaniesHouseSearchItem Item, int Score)? FindBestMatch( string companyName, - List items) + List items, + DateOnly? claimedStartDate) { var normalizedSearch = companyName.ToUpperInvariant(); @@ -568,10 +569,32 @@ public sealed class CompanyVerifierService : ICompanyVerifierService .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(); - 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) diff --git a/src/TrueCV.Web/Components/Pages/Check.razor b/src/TrueCV.Web/Components/Pages/Check.razor index a9c1f1d..a881650 100644 --- a/src/TrueCV.Web/Components/Pages/Check.razor +++ b/src/TrueCV.Web/Components/Pages/Check.razor @@ -76,7 +76,17 @@ - @if (_selectedFiles.Count > 0) + @if (_isBuffering) + { +
+
+ Loading files... +
+

Reading files...

+
+ } + + @if (_selectedFiles.Count > 0 && !_isBuffering) {
@@ -156,8 +166,9 @@ @code { - private List _selectedFiles = new(); + private List _selectedFiles = new(); private bool _isUploading; + private bool _isBuffering; private bool _isDragging; private int _uploadProgress; private string? _errorMessage; @@ -172,6 +183,9 @@ private static readonly byte[] PdfMagicBytes = [0x25, 0x50, 0x44, 0x46]; // %PDF 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() { _isDragging = true; @@ -187,32 +201,57 @@ _isDragging = false; } - private void HandleFileSelected(InputFileChangeEventArgs e) + private async Task HandleFileSelected(InputFileChangeEventArgs e) { _errorMessage = null; + _isBuffering = true; + StateHasChanged(); + var invalidFiles = new List(); var oversizedFiles = new List(); + var failedFiles = new List(); - foreach (var file in e.GetMultipleFiles(MaxFileCount)) + try { - if (!IsValidFileType(file.Name)) + foreach (var file in e.GetMultipleFiles(MaxFileCount)) { - invalidFiles.Add(file.Name); - continue; - } + if (!IsValidFileType(file.Name)) + { + invalidFiles.Add(file.Name); + continue; + } - if (file.Size > MaxFileSize) - { - oversizedFiles.Add(file.Name); - continue; - } + if (file.Size > MaxFileSize) + { + oversizedFiles.Add(file.Name); + continue; + } - // Avoid duplicates - if (!_selectedFiles.Any(f => f.Name == file.Name && f.Size == file.Size)) - { - _selectedFiles.Add(file); + // Avoid duplicates + if (_selectedFiles.Any(f => f.Name == file.Name && f.Size == file.Size)) + { + 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 var errors = new List(); @@ -220,12 +259,14 @@ errors.Add($"Invalid file type(s): {string.Join(", ", invalidFiles.Take(3))}{(invalidFiles.Count > 3 ? $" and {invalidFiles.Count - 3} more" : "")}"); if (oversizedFiles.Count > 0) 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) _errorMessage = string.Join(". ", errors); } - private void RemoveFile(IBrowserFile file) + private void RemoveFile(BufferedFile file) { _selectedFiles.Remove(file); } @@ -269,10 +310,7 @@ try { - await using var stream = file.OpenReadStream(MaxFileSize); - using var memoryStream = new MemoryStream(); - await stream.CopyToAsync(memoryStream); - memoryStream.Position = 0; + using var memoryStream = new MemoryStream(file.Data); // Validate file content (magic bytes) if (!await ValidateFileContentAsync(memoryStream, file.Name))