diff --git a/src/Purrse.Api/Services/ImportService.cs b/src/Purrse.Api/Services/ImportService.cs index 65f493c..dd18cbd 100644 --- a/src/Purrse.Api/Services/ImportService.cs +++ b/src/Purrse.Api/Services/ImportService.cs @@ -218,7 +218,7 @@ public class ImportService : IImportService Date = DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc), Amount = parsed.Amount, PayeeId = matchedPayee?.Id, - PayeeName = parsed.PayeeName, + PayeeName = matchedPayee?.Name ?? parsed.PayeeName, CategoryId = matchedPayee?.DefaultCategoryId, Memo = parsed.Memo, ReferenceNumber = parsed.ReferenceNumber, diff --git a/src/Purrse.Api/Services/PayeeService.cs b/src/Purrse.Api/Services/PayeeService.cs index 2ab916a..ca10f22 100644 --- a/src/Purrse.Api/Services/PayeeService.cs +++ b/src/Purrse.Api/Services/PayeeService.cs @@ -51,6 +51,9 @@ public class PayeeService : IPayeeService _db.Payees.Add(payee); await _db.SaveChangesAsync(); + + await MatchUnlinkedTransactions(userId, payee); + return new PayeeResponse(payee.Id, payee.Name, payee.DefaultCategoryId, null, true, payee.Aliases.Select(a => new PayeeAliasResponse(a.Id, a.Alias)).ToList()); } @@ -65,6 +68,8 @@ public class PayeeService : IPayeeService payee.IsActive = request.IsActive; await _db.SaveChangesAsync(); + await NormalizeLinkedTransactions(payee); + return new PayeeResponse(payee.Id, payee.Name, payee.DefaultCategoryId, null, payee.IsActive, payee.Aliases.Select(a => new PayeeAliasResponse(a.Id, a.Alias)).ToList()); } @@ -78,10 +83,15 @@ public class PayeeService : IPayeeService public async Task AddAliasAsync(Guid userId, Guid payeeId, string alias) { - var payee = await _db.Payees.FirstOrDefaultAsync(p => p.Id == payeeId && p.UserId == userId) + var payee = await _db.Payees.Include(p => p.Aliases) + .FirstOrDefaultAsync(p => p.Id == payeeId && p.UserId == userId) ?? throw new KeyNotFoundException("Payee not found"); _db.PayeeAliases.Add(new PayeeAlias { Id = Guid.NewGuid(), PayeeId = payeeId, Alias = alias }); await _db.SaveChangesAsync(); + + // Reload aliases so the newly added one is included + await _db.Entry(payee).Collection(p => p.Aliases).LoadAsync(); + await MatchUnlinkedTransactions(userId, payee); } public async Task RemoveAliasAsync(Guid userId, Guid payeeId, Guid aliasId) @@ -127,4 +137,66 @@ public class PayeeService : IPayeeService return bestScore >= 0.85 ? bestMatch : null; } + + private async Task NormalizeLinkedTransactions(Payee payee) + { + var transactions = await _db.Transactions + .Where(t => t.PayeeId == payee.Id && t.PayeeName != payee.Name) + .ToListAsync(); + + foreach (var txn in transactions) + txn.PayeeName = payee.Name; + + if (transactions.Count > 0) + await _db.SaveChangesAsync(); + } + + private async Task MatchUnlinkedTransactions(Guid userId, Payee payee) + { + var unlinked = await _db.Transactions + .Include(t => t.Account) + .Where(t => t.Account.UserId == userId && t.PayeeId == null && t.PayeeName != null) + .ToListAsync(); + + var matched = false; + foreach (var txn in unlinked) + { + if (!IsMatch(txn.PayeeName!, payee)) + continue; + + txn.PayeeId = payee.Id; + txn.PayeeName = payee.Name; + + if (txn.CategoryId == null && payee.DefaultCategoryId != null) + txn.CategoryId = payee.DefaultCategoryId; + + matched = true; + } + + if (matched) + await _db.SaveChangesAsync(); + } + + private static bool IsMatch(string transactionPayeeName, Payee payee) + { + if (string.Equals(transactionPayeeName, payee.Name, StringComparison.OrdinalIgnoreCase)) + return true; + + foreach (var alias in payee.Aliases) + { + if (string.Equals(transactionPayeeName, alias.Alias, StringComparison.OrdinalIgnoreCase)) + return true; + } + + if (StringSimilarityHelper.Calculate(transactionPayeeName, payee.Name) >= 0.85) + return true; + + foreach (var alias in payee.Aliases) + { + if (StringSimilarityHelper.Calculate(transactionPayeeName, alias.Alias) >= 0.85) + return true; + } + + return false; + } }