From c0a6b1690fcb0b43b8dbc21453f9b83445feac85 Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 12 Oct 2025 12:58:39 -0400 Subject: [PATCH] Add duplicate detection for receipt uploads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented duplicate detection to warn users when uploading receipts that may already exist in the system. Changes: - Extended ReceiptUploadResult to include DuplicateWarning list - Added CheckForDuplicatesAsync method to ReceiptManager that checks: 1. Identical file content (same SHA256 hash) 2. Same file name and size (potential duplicate saved/edited) - Updated upload flow to check for duplicates before saving - Enhanced Receipts page to: - Display warning alert when duplicates are detected - Show details of each potential duplicate (receipt ID, filename, upload date, mapped transaction, and reason) - Provide quick link to view the duplicate receipt - Use TempData to persist warnings across redirect Duplicate detection runs on every upload and provides actionable information to help users avoid uploading the same receipt multiple times. The check uses hash-based detection for exact duplicates and name+size matching for potential duplicates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- MoneyMap/Pages/Receipts.cshtml | 33 ++++++++++++++ MoneyMap/Pages/Receipts.cshtml.cs | 24 +++++++++- MoneyMap/Services/ReceiptManager.cs | 70 +++++++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 6 deletions(-) diff --git a/MoneyMap/Pages/Receipts.cshtml b/MoneyMap/Pages/Receipts.cshtml index 02f1fa2..f4bc2e3 100644 --- a/MoneyMap/Pages/Receipts.cshtml +++ b/MoneyMap/Pages/Receipts.cshtml @@ -18,6 +18,39 @@ } + +@if (Model.DuplicateWarnings.Any()) +{ + +} +
diff --git a/MoneyMap/Pages/Receipts.cshtml.cs b/MoneyMap/Pages/Receipts.cshtml.cs index 4bb81cc..b634a6b 100644 --- a/MoneyMap/Pages/Receipts.cshtml.cs +++ b/MoneyMap/Pages/Receipts.cshtml.cs @@ -29,9 +29,20 @@ namespace MoneyMap.Pages [TempData] public bool IsSuccess { get; set; } + [TempData] + public string? DuplicateWarningsJson { get; set; } + + public List DuplicateWarnings { get; set; } = new(); + public async Task OnGetAsync() { await LoadReceiptsAsync(); + + // Deserialize duplicate warnings if present + if (!string.IsNullOrWhiteSpace(DuplicateWarningsJson)) + { + DuplicateWarnings = System.Text.Json.JsonSerializer.Deserialize>(DuplicateWarningsJson) ?? new(); + } } public async Task OnPostUploadAsync() @@ -48,8 +59,17 @@ namespace MoneyMap.Pages if (result.IsSuccess) { - Message = "Receipt uploaded successfully!"; - IsSuccess = true; + if (result.DuplicateWarnings.Any()) + { + Message = $"Receipt uploaded successfully, but {result.DuplicateWarnings.Count} potential duplicate(s) detected."; + IsSuccess = true; + DuplicateWarningsJson = System.Text.Json.JsonSerializer.Serialize(result.DuplicateWarnings); + } + else + { + Message = "Receipt uploaded successfully!"; + IsSuccess = true; + } return RedirectToPage(); } else diff --git a/MoneyMap/Services/ReceiptManager.cs b/MoneyMap/Services/ReceiptManager.cs index 73d0c7b..dd8605a 100644 --- a/MoneyMap/Services/ReceiptManager.cs +++ b/MoneyMap/Services/ReceiptManager.cs @@ -86,7 +86,7 @@ namespace MoneyMap.Services fileHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); } - // Check for duplicate (same transaction + same hash, or same hash if unmapped) + // Check for exact duplicate (same transaction + same hash) if (transactionId.HasValue) { var existingReceipt = await _db.Receipts @@ -96,6 +96,9 @@ namespace MoneyMap.Services return ReceiptUploadResult.Failure("This receipt has already been uploaded for this transaction."); } + // Check for potential duplicates (same hash, same name+size) + var duplicateWarnings = await CheckForDuplicatesAsync(fileHash, file.FileName, file.Length); + // Generate unique filename var storedFileName = $"{transactionId?.ToString() ?? "unmapped"}_{Guid.NewGuid()}{extension}"; var filePath = Path.Combine(receiptsBasePath, storedFileName); @@ -125,7 +128,55 @@ namespace MoneyMap.Services _db.Receipts.Add(receipt); await _db.SaveChangesAsync(); - return ReceiptUploadResult.Success(receipt); + return ReceiptUploadResult.Success(receipt, duplicateWarnings); + } + + private async Task> CheckForDuplicatesAsync(string fileHash, string fileName, long fileSize) + { + var warnings = new List(); + + // Check for receipts with same hash + var hashMatches = await _db.Receipts + .Include(r => r.Transaction) + .Where(r => r.FileHashSha256 == fileHash) + .ToListAsync(); + + foreach (var match in hashMatches) + { + warnings.Add(new DuplicateWarning + { + ReceiptId = match.Id, + FileName = match.FileName, + UploadedAtUtc = match.UploadedAtUtc, + TransactionId = match.TransactionId, + TransactionName = match.Transaction?.Name, + Reason = "Identical file content (same hash)" + }); + } + + // Check for receipts with same name and size (but different hash - might be resaved/edited) + if (!warnings.Any()) + { + var nameAndSizeMatches = await _db.Receipts + .Include(r => r.Transaction) + .Where(r => r.FileName == fileName && r.FileSizeBytes == fileSize) + .ToListAsync(); + + foreach (var match in nameAndSizeMatches) + { + warnings.Add(new DuplicateWarning + { + ReceiptId = match.Id, + FileName = match.FileName, + UploadedAtUtc = match.UploadedAtUtc, + TransactionId = match.TransactionId, + TransactionName = match.Transaction?.Name, + Reason = "Same file name and size" + }); + } + } + + return warnings; } public async Task MapReceiptToTransactionAsync(long receiptId, long transactionId) @@ -227,11 +278,22 @@ namespace MoneyMap.Services public bool IsSuccess { get; init; } public Receipt? Receipt { get; init; } public string? ErrorMessage { get; init; } + public List DuplicateWarnings { get; init; } = new(); - public static ReceiptUploadResult Success(Receipt receipt) => - new() { IsSuccess = true, Receipt = receipt }; + public static ReceiptUploadResult Success(Receipt receipt, List? warnings = null) => + new() { IsSuccess = true, Receipt = receipt, DuplicateWarnings = warnings ?? new() }; public static ReceiptUploadResult Failure(string error) => new() { IsSuccess = false, ErrorMessage = error }; } + + public class DuplicateWarning + { + public long ReceiptId { get; set; } + public string FileName { get; set; } = ""; + public DateTime UploadedAtUtc { get; set; } + public long? TransactionId { get; set; } + public string? TransactionName { get; set; } + public string Reason { get; set; } = ""; + } } \ No newline at end of file