Normalize PayeeName on import and propagate payee changes to transactions
Use clean payee name instead of raw bank string when a payee match is found during import. When payees are created/renamed or aliases added, automatically update or link matching unlinked transactions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user