using Books.Api.EventFlow.ReadModels; using Books.Api.EventFlow.Repositories; namespace Books.Api.AiBookkeeper; /// /// Matches documents to pending bank transactions based on amount. /// public class BankTransactionMatcher( IBankTransactionRepository transactionRepository, ILogger logger) : IBankTransactionMatcher { public async Task FindMatchingTransactionAsync( string companyId, decimal amount, decimal tolerance = 0.01m, CancellationToken cancellationToken = default) { // For invoice/expense: document shows positive amount, but bank transaction is negative (money leaving) // We need to match on the absolute value var targetAmount = amount; logger.LogDebug( "Searching for pending transaction matching amount {Amount} (±{Tolerance}) for company {CompanyId}", targetAmount, tolerance, companyId); var pendingTransactions = await transactionRepository.GetPendingByCompanyIdAsync(companyId, cancellationToken); if (pendingTransactions.Count == 0) { logger.LogDebug("No pending transactions found for company {CompanyId}", companyId); return null; } // Find transactions matching the amount within tolerance // For expenses: document amount is positive, bank amount is negative // So we compare: |bank amount| matches document amount var matchingTransactions = pendingTransactions .Where(t => IsAmountMatch(t.Amount, targetAmount, tolerance)) .OrderBy(t => t.TransactionDate) // Oldest first .ThenBy(t => t.CreatedAt) .ToList(); if (matchingTransactions.Count == 0) { logger.LogDebug( "No matching transactions found for amount {Amount} (checked {Count} pending)", targetAmount, pendingTransactions.Count); return null; } var match = matchingTransactions.First(); logger.LogInformation( "Found matching transaction {TransactionId}: {Amount} on {Date} ({Description})", match.Id, match.Amount, match.TransactionDate.ToString("yyyy-MM-dd"), match.Description); if (matchingTransactions.Count > 1) { logger.LogDebug( "Multiple matches found ({Count}), returning oldest", matchingTransactions.Count); } return match; } private static bool IsAmountMatch(decimal bankAmount, decimal documentAmount, decimal tolerance) { // For expenses: document amount is typically positive (e.g., invoice for 1000 DKK) // Bank transaction is negative (e.g., -1000 DKK outgoing payment) // So we need to check if the absolute values match // Option 1: Exact match on absolute values (most common for invoice matching) var absBank = Math.Abs(bankAmount); var absDoc = Math.Abs(documentAmount); if (Math.Abs(absBank - absDoc) <= tolerance) { return true; } // Option 2: Direct match (same sign and value) if (Math.Abs(bankAmount - documentAmount) <= tolerance) { return true; } // Option 3: Opposite sign match (document positive, bank negative or vice versa) if (Math.Abs(bankAmount + documentAmount) <= tolerance) { return true; } return false; } }