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