using System; using System.Collections.Generic; using System.Linq; using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets.Reports; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; using Schaad.Finance.Api; using Schaad.Finance.Api.Datasets; namespace Schaad.Accounting.Services { public class ViewService : IViewService { private readonly IAccountRepository accountRepository; private readonly IBankTransactionRepository bankTransactionRepository; private readonly IBookingRuleRepository bookingRuleRepository; private readonly ISubclassRepository subclassRepository; private readonly ITransactionRepository transactionRepository; private readonly IFxService fxService; private readonly ISettingsService settingsService; public ViewService( IAccountRepository accountRepository, IBankTransactionRepository bankTransactionRepository, ITransactionRepository transactionRepository, ISubclassRepository subclassRepository, IBookingRuleRepository bookingRuleRepository, IFxService fxService, ISettingsService settingsService) { this.accountRepository = accountRepository; this.bankTransactionRepository = bankTransactionRepository; this.transactionRepository = transactionRepository; this.subclassRepository = subclassRepository; this.bookingRuleRepository = bookingRuleRepository; this.fxService = fxService; this.settingsService = settingsService; } /// /// Load accounts /// public AccountDataset GetAccountView(string id) { var accounts = GetAccountViewList(); return accounts.Single(t => t.Id == id); } /// /// Load accounts and calculates their balances /// public List GetAccountViewList() { var accounts = accountRepository.GetAccountList(); var transactionList = GetTransactionViewList(); var subClassNameByNumber = subclassRepository.GetSubClassList().ToDictionary(s => s.Number, s => s.Name); var settings = settingsService.GetSettings(); // Group transactions by account so per-account balance is O(k) instead of O(M). var creditsByAccount = transactionList.GroupBy(t => t.TargetAccountId).ToDictionary(g => g.Key, g => g.ToList()); var debitsByAccount = transactionList.GroupBy(t => t.OriginAccountId).ToDictionary(g => g.Key, g => g.ToList()); var accountList = accounts.Select(a => { var balance = ComputeBalance(a, creditsByAccount, debitsByAccount); return new AccountDataset( account: a, balance: balance, balanceCHF: fxService.ConvertCurrency(balance, a.Currency, "CHF", settings.FixerIoApiKey), startBalanceCHF: fxService.ConvertCurrency(a.StartBalance, a.Currency, "CHF", settings.FixerIoApiKey), className: subclassRepository.GetClass(a.Class), subClassName: subClassNameByNumber[a.SubClass]); }) .ToList(); return accountList.OrderBy(a => a.Number).ToList(); } private static decimal ComputeBalance( Account account, Dictionary> creditsByAccount, Dictionary> debitsByAccount) { var balance = account.StartBalance; if (creditsByAccount.TryGetValue(account.Id, out var credits)) { balance += credits.Sum(t => t.GetValue(account.IsFxAccount)); } if (debitsByAccount.TryGetValue(account.Id, out var debits)) { balance -= debits.Sum(t => t.GetValue(account.IsFxAccount)); } return balance; } public BalanceDataset GetBalanceView() { var accountList = GetAccountViewList(); var balanceView = new BalanceDataset( activaAccountList: accountList.Where(m => m.Class == ClassIds.Activa).ToList(), passivaAccountList: accountList.Where(m => m.Class == ClassIds.Passiva).ToList(), totalActivaCHF: Math.Abs(accountList.Where(m => m.Class == ClassIds.Activa).Sum(m => m.BalanceCHF)), totalPassivaCHF: Math.Abs(accountList.Where(m => m.Class == ClassIds.Passiva).Sum(m => m.BalanceCHF)) ); return balanceView; } public BalanceSheetDataset GetBalanceSheetView(int year) { var accountList = GetAccountViewList(); var settings = settingsService.GetSettings(); var profit = Math.Abs(accountList.Where(m => m.Class == ClassIds.Income).Sum(m =>m.BalanceCHF)); profit += Math.Abs(accountList.Where(m => m.Class == ClassIds.Activa).Sum(m => fxService.ConvertCurrency(m.StartBalance, m.Currency, "CHF", settings.FixerIoApiKey))); var loss = Math.Abs(accountList.Where(m => m.Class == ClassIds.Expenses).Sum(m => m.BalanceCHF)); var balanceView = new BalanceSheetDataset( activaAccountList: accountList.Where(m => m.Class == ClassIds.Activa).ToList(), incomeAccountList: accountList.Where(m => m.Class == ClassIds.Income).ToList(), expensesAccountList: accountList.Where(m => m.Class == ClassIds.Expenses).ToList(), profitCHF: profit, lossCHF: loss, year: year ); return balanceView; } /// /// Get transaction list with the origin and target account for each transaction /// public List GetTransactionViewList() { var accountsById = accountRepository.GetAccountList().ToDictionary(a => a.Id); var transactionList = transactionRepository.GetTransactionList(); return transactionList.Select(t => new TransactionDataset( t, accountsById[t.OriginAccountId], accountsById[t.TargetAccountId])) .ToList(); } /// /// Get transaction list with the origin and target account for each transaction /// public List GetTransactionViewList(string accountId) { var accountsById = accountRepository.GetAccountList().ToDictionary(a => a.Id); var transactionList = transactionRepository.GetTransactionList() .Where(t => t.OriginAccountId == accountId || t.TargetAccountId == accountId); return transactionList.Select(t => new TransactionDataset( WithDisplaySign(t), accountsById[t.OriginAccountId], accountsById[t.TargetAccountId])) .ToList(); Transaction WithDisplaySign(Transaction t) { var originAccount = accountsById[t.OriginAccountId]; if (originAccount.Class == ClassIds.Activa && accountId == t.OriginAccountId) { var copy = new Transaction(); t.Copy(copy); copy.Value *= -1; return copy; } return t; } } /// /// Get booking rules with their account /// public List GetBookingRuleViewList() { var accountsById = accountRepository.GetAccountList().ToDictionary(a => a.Id); var bookingRules = bookingRuleRepository.GetBookingRuleList(); return bookingRules.Select(t => new BookingRuleDataset(t, accountsById[t.AccountId].Name)) .ToList(); } /// /// Load open bank transactions /// public List GetOpenBankTransactionList() { var transactions = transactionRepository .GetTransactionList() .Where(t => string.IsNullOrEmpty(t.BankTransactionId) == false) .Select(t => t.BankTransactionId); var bankTransactions = bankTransactionRepository .GetBankTransactionList() .Where(b => b.Ignore == false && transactions.Contains(b.Id) == false) .ToList(); return bankTransactions; } public List MatchOpenBankTransactions() { var newTransactionList = new List(); var transactions = transactionRepository.GetTransactionList(); var accounts = accountRepository.GetAccountList(); var bookingRules = bookingRuleRepository.GetBookingRuleList(); var bankTransactions = GetOpenBankTransactionList().OrderBy(t => t.ValueDate); foreach (var bankTransaction in bankTransactions) { var trx = new Transaction(bankTransaction, accounts); MatchBankTransactionByBookingRule(bankTransaction.Text, trx, bookingRules); MatchBankTransactionBySameAccountsLastMonth(bankTransaction, trx, transactions); MatchBankTransactionBySameValueLastMonth(bankTransaction, trx, transactions); newTransactionList.Add(trx); } return newTransactionList; } public List MatchCreditCardTransactions(string bankTransactionId, IReadOnlyList creditCardTransactions) { var accounts = accountRepository.GetAccountList(); var bankTrx = bankTransactionRepository.GetBankTransaction(bankTransactionId); var bookingRules = bookingRuleRepository.GetBookingRuleList(); var trxList = new List(); foreach (var creditCardTransaction in creditCardTransactions) { var trx = new Transaction(bankTrx, accounts); trx.Value = -1 * creditCardTransaction.Amount; trx.BookingDate = bankTrx.BookingDate; trx.ValueDate = creditCardTransaction.TransactionDate; MatchBankTransactionByBookingRule(creditCardTransaction.Transaction, trx, bookingRules); if (string.IsNullOrEmpty(trx.Text)) { trx.Text = creditCardTransaction.Transaction; } trxList.Add(trx); } return trxList; } private void MatchBankTransactionByBookingRule(string transactionText, Transaction trx, List bookingRules) { if (string.IsNullOrEmpty(trx.Text) == false) return; // all booking rules that match the text var matchedRules = bookingRules.Where(r => transactionText.ToLowerInvariant().Contains(r.LookupText.ToLowerInvariant())).ToList(); // then match by value var matchedRule = matchedRules.FirstOrDefault(r => r.LookupValue == Math.Abs(trx.Value)); // if no found, take match without a value if (matchedRule == null) { matchedRule = matchedRules.FirstOrDefault(r => r.LookupValue == 0); } if (matchedRule != null) { if (string.IsNullOrEmpty(trx.OriginAccountId)) { trx.OriginAccountId = matchedRule.AccountId; } else { trx.TargetAccountId = matchedRule.AccountId; } trx.Text = matchedRule.BookingText; } } private void MatchBankTransactionBySameAccountsLastMonth(BankTransaction bankTransaction, Transaction trx, List transactions) { if (string.IsNullOrEmpty(trx.Text) == false) return; var lastMonth = trx.ValueDate.AddMonths(-1); var lastMonthStart = new DateTime(lastMonth.Year, lastMonth.Month, 1); var lastMonthEnd = lastMonthStart.AddMonths(1).AddMinutes(-1); // Find all bookings for the same accounts of the last month var lastMonthTransactions = transactions.Where( t => t.OriginAccountId == trx.OriginAccountId && t.TargetAccountId == trx.TargetAccountId && Math.Abs(t.Value) == Math.Abs(trx.Value) && t.ValueDate >= lastMonthStart && t.ValueDate <= lastMonthEnd) .ToList(); MatchSameOrOtherDay(trx, lastMonthTransactions); } private void MatchBankTransactionBySameValueLastMonth(BankTransaction bankTransaction, Transaction trx, List transactions) { if (string.IsNullOrEmpty(trx.Text) == false) return; var lastMonth = trx.ValueDate.AddMonths(-1); var lastMonthStart = new DateTime(lastMonth.Year, lastMonth.Month, 1); var lastMonthEnd = lastMonthStart.AddMonths(1).AddMinutes(-1); // Find all bookings for the same amount of the last month var lastMonthTransactions = transactions.Where( t => Math.Abs(t.Value) == Math.Abs(trx.Value) && t.ValueDate >= lastMonthStart && t.ValueDate <= lastMonthEnd) .ToList(); MatchSameOrOtherDay(trx, lastMonthTransactions); } private void MatchSameOrOtherDay(Transaction trx, List lastMonthTransactions) { if (lastMonthTransactions.Any()) { var lastMonthTransaction = lastMonthTransactions.Where( t => t.ValueDate.Day == trx.ValueDate.Day) .FirstOrDefault(); if (lastMonthTransaction == null) { lastMonthTransaction = lastMonthTransactions.Where( t => t.ValueDate.Day >= trx.ValueDate.AddDays(-1).Day && t.ValueDate.Day <= trx.ValueDate.AddDays(1).Day) .FirstOrDefault(); } if (lastMonthTransaction != null) { trx.Text = lastMonthTransaction.Text; if (string.IsNullOrEmpty(trx.OriginAccountId)) { trx.OriginAccountId = lastMonthTransaction.OriginAccountId; } else { trx.TargetAccountId = lastMonthTransaction.TargetAccountId; } } } } } }