using Books.Api.EventFlow.ReadModels; using Books.Api.EventFlow.Repositories; namespace Books.Api.AiBookkeeper; /// /// Maps standard account numbers to company-specific accounts. /// Uses the standardAccountNumber field on accounts for matching. /// public class AccountMappingService( IAccountRepository accountRepository, ILogger logger) : IAccountMappingService { public async Task FindByStandardAccountNumberAsync( string companyId, string standardAccountNumber, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(standardAccountNumber)) { return null; } var accounts = await accountRepository.GetActiveByCompanyIdAsync(companyId, cancellationToken); // First, try exact match on standardAccountNumber var exactMatch = accounts.FirstOrDefault(a => a.StandardAccountNumber != null && a.StandardAccountNumber.Equals(standardAccountNumber, StringComparison.OrdinalIgnoreCase)); if (exactMatch != null) { logger.LogDebug( "Found exact match for standard account {StandardAccount}: {AccountNumber} - {AccountName}", standardAccountNumber, exactMatch.AccountNumber, exactMatch.Name); return exactMatch; } // Try prefix match (standard account numbers can be hierarchical) var prefixMatch = accounts .Where(a => a.StandardAccountNumber != null && standardAccountNumber.StartsWith(a.StandardAccountNumber, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(a => a.StandardAccountNumber?.Length ?? 0) .FirstOrDefault(); if (prefixMatch != null) { logger.LogDebug( "Found prefix match for standard account {StandardAccount}: {AccountNumber} - {AccountName}", standardAccountNumber, prefixMatch.AccountNumber, prefixMatch.Name); return prefixMatch; } logger.LogDebug("No match found for standard account {StandardAccount}", standardAccountNumber); return null; } public async Task> MapSuggestedLinesAsync( string companyId, List suggestedLines, CancellationToken cancellationToken = default) { var accounts = await accountRepository.GetActiveByCompanyIdAsync(companyId, cancellationToken); // Index accounts by both StandardAccountNumber and direct AccountNumber var accountsByStandardNumber = accounts .Where(a => !string.IsNullOrEmpty(a.StandardAccountNumber)) .GroupBy(a => a.StandardAccountNumber!) .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); var accountsByNumber = accounts .ToDictionary(a => a.AccountNumber, a => a, StringComparer.OrdinalIgnoreCase); var result = new List(); foreach (var line in suggestedLines) { AccountReadModelDto? mappedAccount = null; if (!string.IsNullOrEmpty(line.StandardAccountNumber)) { // First, try direct account number match (AI service returns company account numbers) if (accountsByNumber.TryGetValue(line.StandardAccountNumber, out var directMatch)) { mappedAccount = directMatch; } // Then try Erhvervsstyrelsen standard account number match else if (accountsByStandardNumber.TryGetValue(line.StandardAccountNumber, out var exactMatch)) { mappedAccount = exactMatch; } else { // Try prefix match on standard numbers mappedAccount = accountsByStandardNumber .Where(kvp => line.StandardAccountNumber.StartsWith(kvp.Key, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(kvp => kvp.Key.Length) .Select(kvp => kvp.Value) .FirstOrDefault(); } } result.Add(new MappedSuggestedLine { Original = line, MappedAccount = mappedAccount }); if (mappedAccount != null) { logger.LogDebug( "Mapped standard account {StandardAccount} to {AccountNumber} - {AccountName}", line.StandardAccountNumber, mappedAccount.AccountNumber, mappedAccount.Name); } else { logger.LogDebug( "Could not map standard account {StandardAccount}", line.StandardAccountNumber); } } return result; } }