com.cubeia.backoffice.accounting.core.manager.AccountingManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.cubeia.backoffice.accounting.core.manager.AccountingManagerImpl.java

Source

/**
 * Copyright (C) 2010 Cubeia Ltd <info@cubeia.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.cubeia.backoffice.accounting.core.manager;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import javax.annotation.Resource;

import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.cubeia.backoffice.accounting.api.Money;
import com.cubeia.backoffice.accounting.api.NegativeBalanceException;
import com.cubeia.backoffice.accounting.api.NoSuchAccountException;
import com.cubeia.backoffice.accounting.core.AccountClosedException;
import com.cubeia.backoffice.accounting.core.AccountNotFoundException;
import com.cubeia.backoffice.accounting.core.TransactionNotBalancedException;
import com.cubeia.backoffice.accounting.core.TransactionNotFoundException;
import com.cubeia.backoffice.accounting.core.dao.AccountingDAO;
import com.cubeia.backoffice.accounting.core.dao.EntrySumAndCount;
import com.cubeia.backoffice.accounting.core.domain.AccountsOrder;
import com.cubeia.backoffice.accounting.core.domain.BalancedEntry;
import com.cubeia.backoffice.accounting.core.domain.QueryResultsContainer;
import com.cubeia.backoffice.accounting.core.domain.TransactionParticipant;
import com.cubeia.backoffice.accounting.core.domain.TransactionsOrder;
import com.cubeia.backoffice.accounting.core.entity.Account;
import com.cubeia.backoffice.accounting.core.entity.AccountAttribute;
import com.cubeia.backoffice.accounting.core.entity.AccountStatus;
import com.cubeia.backoffice.accounting.core.entity.BalanceCheckpoint;
import com.cubeia.backoffice.accounting.core.entity.CurrencyRate;
import com.cubeia.backoffice.accounting.core.entity.Entry;
import com.cubeia.backoffice.accounting.core.entity.Transaction;
import com.cubeia.backoffice.accounting.core.integrations.PluginManager;
import com.cubeia.backoffice.accounting.core.util.ExecTimeCollector;

/*
 *  NOTE: This component is transactional via the spring
 *  config, using AOP advises
 */
@Component("accounting.accountingManager")
public class AccountingManagerImpl implements AccountingManager {

    // public static final long CHECKPOINT_FLUSH_DELAY = 2000;
    private static final Object DUMMY_PAYLOAD = new Object();

    // --- INSTANCE MEMBERS --- // 

    /*
     * Set to true to allow transactions involving closed accounts
     */
    private AtomicBoolean allowClosedAccounts = new AtomicBoolean(false);

    /*
     * Default checkpoint interval (per account).
     */
    private int balanceCheckpointInterval = 20;

    /*
     * Your standard logger...
     */
    private Logger log = LoggerFactory.getLogger(getClass());

    /*
     * Set to true to assume an external thread calls 
     * checkAndCreateCheckpoints() regularly.
     */
    private AtomicBoolean asyncCheckpointCreation = new AtomicBoolean(true);

    /*
     * This map contains account ID's mapped to dummy objects for accounts that are known
     * to have more than the limit of check point intervals. The dirty state is checked on 
     * "get balance" only.
     */
    private Map<Long, Object> dirtyAccountSet = new ConcurrentHashMap<Long, Object>();

    /*
     * Used by scheduled thread to mark failed checkpoints
     */
    private Set<Long> errorSet = Collections.synchronizedSet(new HashSet<Long>());

    private final AtomicReference<ExecTimeCollector> checkPointTime = new AtomicReference<ExecTimeCollector>();

    // --- DAOS -- //

    @Resource(name = "accounting.accountingDAO")
    private AccountingDAO accountingDAO;

    @Resource(name = "accounting.pluginManager")
    private PluginManager pluginManager;

    // --- PUBLIC METHODS --- //

    @Override
    public Transaction getTransactionByExternalId(String extId) {
        return accountingDAO.getTransactionByExternalId(extId);
    }

    @Override
    public Map<Long, Account> getAccounts(Set<Long> ids) {
        Map<Long, Account> map = new HashMap<Long, Account>(ids.size());
        for (Long id : ids) {
            Account a = accountingDAO.getAccount(id);
            if (a == null) {
                throw new NoSuchAccountException(id);
            }
            map.put(id, a);
        }
        return map;
    }

    @Override
    public BalanceCheckpoint getLastCheckpoint(Long accountId) {
        return accountingDAO.getLatestBalanceCheckpoint(accountId, null);
    }

    @Override
    public void setAllowTransactionsWithClosedAccounts(boolean allow) {
        log.info("Allow transactions with closed accounts set to: " + allow);
        allowClosedAccounts.set(allow);
    }

    @Override
    public boolean getAllowTransactionsWithClosedAccounts() {
        return allowClosedAccounts.get();
    }

    @Override
    public void setCheckpointExecTimeCollector(ExecTimeCollector col) {
        checkPointTime.set(col);
    }

    @Override
    public void checkAndCreateCheckpoints() {
        // dirtyAccountSet.keySet()HashSet<Long> accountIds = new HashSet<Long>(dirtyAccountSet.keySet());
        long time = System.currentTimeMillis();
        // int size = dirtyAccountSet.keySet().size();
        // int count = 0;
        for (Long accountId : dirtyAccountSet.keySet()) {
            /*
             * Roll through all dirty accounts, create checkpoint if the entry count
             * is larger than the configured interval, remove from dirty set and error
             * set.
             */
            try {
                createCheckpointIfNeeded(accountId);
                dirtyAccountSet.remove(accountId);
                errorSet.remove(accountId);
                // count++;
            } catch (Throwable th) {
                if (errorSet.contains(accountId)) {
                    log.error("Failed to set consecutive checkpoint for account " + accountId, th);
                } else {
                    log.warn("Failed to set first checkpoint for account " + accountId + ", will retry", th);
                    errorSet.add(accountId);
                }
            }
        }
        /*
         * Report timing if needed.
         */
        long t = System.currentTimeMillis() - time;
        ExecTimeCollector col = checkPointTime.get();
        // log.debug("Scheduled checkpoint creation created " + count + " checkpoints (size before exec: " + size + ") in " + t + " millis");
        if (col != null) {
            col.report(t);
        }
    }

    @Override
    public void setTransactionExternalId(Long transactionId, String externalId) throws SecurityException {
        Transaction trans = getTransactionById(transactionId);
        if (trans == null)
            throw new IllegalArgumentException("No such transaction: " + transactionId);
        if (trans.getExternalId() != null) {
            throw new SecurityException("Transaction " + transactionId
                    + " already have an extranal id, it is illegal to set this value twice.");
        } else {
            trans.setExternalId(externalId);
            accountingDAO.saveOrUpdate(trans);
        }
    }

    @Override
    public void updateAccount(Account account) {
        accountingDAO.merge(account);
    }

    @Override
    public void removeAccountAttribute(Long accountId, String key) {
        Account a = accountingDAO.getAccount(accountId);
        checkThrowNotExists(a, accountId);
        a.getAttributes().remove(key);
    }

    @Override
    public void removeTransactionAttribute(Long transactionId, String key) throws TransactionNotFoundException {
        Transaction t = getTransactionById(transactionId);
        checkThrowNotExists(t, transactionId);
        t.getAttributes().remove(key);
    }

    @Override
    public void setAccountAttribute(Long accountId, String key, String value) {
        Account a = accountingDAO.getAccount(accountId);
        checkThrowNotExists(a, accountId);
        AccountAttribute att = new AccountAttribute(a, key, value);
        a.getAttributes().put(key, att);
    }

    @Override
    public void setTransactionAttribute(Long transactionId, String key, String value)
            throws TransactionNotFoundException {
        setTransactionAttributes(transactionId, Collections.singletonMap(key, value));
    }

    @Override
    public void setTransactionAttributes(Long transactionId, Map<String, String> values)
            throws TransactionNotFoundException {
        Transaction t = getTransactionById(transactionId);
        checkThrowNotExists(t, transactionId);
        t.setStringAttributes(values);
    }

    @Override
    public void setAccountStatus(Long accountId, AccountStatus status) {
        Account a = accountingDAO.getAccount(accountId);
        checkThrowNotExists(a, accountId);
        a.setStatus(status);
    }

    @Override
    public Account createAccount(Account account) {
        checkRequiredFields(account);
        pluginManager.beforeCreate(account);
        accountingDAO.saveOrUpdate(account);
        pluginManager.afterCreate(account);
        return account;
    }

    @Override
    public Account createAccountWithInitialBalance(Account account, BigDecimal balance) {
        checkRequiredFields(account);
        pluginManager.beforeCreate(account);
        accountingDAO.saveOrUpdate(account);
        accountingDAO.saveOrUpdate(new Entry(account, balance));
        pluginManager.afterCreate(account);
        return account;
    }

    @Override
    public Account getAccount(Long accountId) {
        return accountingDAO.getAccount(accountId);
    }

    @Override
    public Collection<Account> getAccountsByUserIdAndCurrency(Long userId, String currency) {
        return accountingDAO.findAccountsByUserId(userId, null, currency);
    }

    @Override
    public Collection<Account> getAccountsByUserAndWalletId(Long userId, Long walletId) {
        return accountingDAO.findAccountsByUserAndWalletId(userId, walletId);
    }

    @Override
    public Collection<Account> getAccountsByUserIdWalletTypeAndCurrency(Long userId, Long walletId, String type,
            String currency) {
        return accountingDAO.findAccountsByUserAndWalletId(userId, walletId, type, currency);
    }

    @Override
    public Collection<Account> getAccountsByTypeAndCurrency(String type, String currency) {
        return accountingDAO.findAccountsByTypeAndCurrency(type, currency);
    }

    @Override
    public Collection<Account> getAccountsByUserId(Long userId) {
        return accountingDAO.findAccountsByUserId(userId);
    }

    @Override
    public Money getBalance(Long accountId) {
        Account account = getAccount(accountId);
        checkThrowNotExists(account, accountId);

        // get latest balance checkpoint
        BalanceCheckpoint balanceCP = accountingDAO.getLatestBalanceCheckpoint(accountId, null);
        BigDecimal balance = calculateBalanceFromCheckpointToEntry(accountId, (long) Integer.MAX_VALUE, balanceCP);

        return new Money(account.getCurrencyCode(), account.getFractionalDigits(), balance);
    }

    @Override
    public Money getBalanceAfterEntry(Long accountId, Long entryId) {
        Account account = getAccount(accountId);
        checkThrowNotExists(account, accountId);

        // get previous balance checkpoint
        BalanceCheckpoint previousCP = accountingDAO.getLatestBalanceCheckpoint(accountId, entryId);
        BigDecimal balance = calculateBalanceFromCheckpointToEntry(accountId, entryId, previousCP);

        return new Money(account.getCurrencyCode(), account.getFractionalDigits(), balance);
    }

    @Override
    public Transaction createTransaction(String comment, String externalId, BigDecimal amount, Long fromAccountId,
            Long toAccountId, Map<String, String> atts) {

        Account fromAccount = getAccount(fromAccountId);
        Account toAccount = getAccount(toAccountId);

        if (toAccount == null) {
            throw new NoSuchAccountException(toAccountId);
        }
        if (fromAccount == null) {
            throw new NoSuchAccountException(fromAccountId);
        }

        Entry fromEntry = new Entry(fromAccount, amount.negate());
        Entry toEntry = new Entry(toAccount, amount);

        return createTransactionInternal(comment, externalId, Arrays.asList(fromEntry, toEntry), atts);
    }

    @Override
    public Transaction createTransaction(String comment, String extId, List<Entry> entries,
            Map<String, String> attributes)
            throws AccountNotFoundException, AccountClosedException, TransactionNotBalancedException {

        return createTransactionInternal(comment, extId, entries, attributes);
    }

    @Override
    public QueryResultsContainer<Account> listAccounts(Long accountId, Long userId, Long walletId,
            String currencyCode, Set<AccountStatus> statuses, Set<String> types, int offset, int limit,
            AccountsOrder order, boolean ascending) {
        List<Account> accounts = accountingDAO.listAccounts(accountId, userId, walletId, currencyCode, statuses,
                types, offset, limit, order, ascending);
        if (log.isTraceEnabled()) {
            log.trace("List accounts from DAOs: " + accounts);
        }
        int size = (int) accountingDAO.countAccounts(accountId, userId, walletId, currencyCode, statuses, types);
        log.debug("List accounts Size from DAOs: " + size);
        return new QueryResultsContainer<Account>(size, accounts);
    }

    @Override
    public Transaction reverseTransaction(Long txId, String newExtId, Map<String, String> oldAttr,
            Map<String, String> newAttr) throws TransactionNotFoundException {
        Transaction trans = accountingDAO.getTransaction(txId);
        if (trans == null) {
            // transaction must exist
            throw new TransactionNotFoundException(txId);
        }

        // reverse and set external id
        Transaction reverser = trans.reverse();
        if (newExtId != null) {
            reverser.setExternalId(newExtId);
        }

        // set new attributes on reversed transaction
        if (oldAttr != null) {
            for (String key : oldAttr.keySet()) {
                if (trans.getAttributes().containsKey(key)) {
                    throw new SecurityException("Transaction " + txId + " already has attribute by name " + key);
                } else {
                    trans.setAttribute(key, oldAttr.get(key));
                }
            }
        }

        // set new attributes on reverser
        if (newAttr != null) {
            for (String key : newAttr.keySet()) {
                if (reverser.getAttributes().containsKey(key)) {
                    throw new SecurityException("Transaction " + txId + " already has attribute by name " + key);
                } else {
                    reverser.setAttribute(key, newAttr.get(key));
                }
            }
        }

        // save new entries
        for (Entry e : reverser.getEntries()) {
            accountingDAO.saveOrUpdate(e);
        }

        // save transactions
        accountingDAO.saveOrUpdate(trans);
        accountingDAO.saveOrUpdate(reverser);

        pluginManager.afterCreate(reverser);

        return reverser;
    }

    @Override
    public QueryResultsContainer<Transaction> listTransactions(TransactionParticipant part1,
            TransactionParticipant part2, Date startDate, Date endDate, int offset, int limit,
            TransactionsOrder order, boolean ascending) {

        List<Transaction> transactions = accountingDAO.listTransactions(part1, part2, startDate, endDate, offset,
                limit, order, ascending);

        int size = accountingDAO.countTransactions(part1, part2, startDate, endDate);

        return new QueryResultsContainer<Transaction>(size, transactions);
    }

    @Override
    public QueryResultsContainer<Entry> listEntries(Long accountId, int offset, int limit, boolean ascending) {
        List<Entry> entries = accountingDAO.listEntries(accountId, offset, limit, ascending);
        int size = (int) accountingDAO.countEntries(accountId);
        return new QueryResultsContainer<Entry>(size, entries);
    }

    @Override
    public QueryResultsContainer<BalancedEntry> listEntriesBalanced(Long accountId, int offset, int limit,
            boolean ascending) {
        List<Entry> entries = accountingDAO.listEntries(accountId, offset, limit, ascending);
        int size = (int) accountingDAO.countEntries(accountId);
        List<BalancedEntry> tmp = new ArrayList<BalancedEntry>(entries.size());
        if (entries.size() > 0 && accountId != null) {
            // Account a = getAccount(accountId);
            Entry one = entries.get(0);
            BigDecimal balance = getBalanceAfterEntry(accountId, one.getId()).getAmount();
            if (ascending) {
                balance = balance.subtract(one.getAmount());
            }
            for (Entry e : entries) {
                if (ascending) {
                    balance = balance.add(e.getAmount());
                }
                tmp.add(new BalancedEntry(e, balance));
                if (!ascending) {
                    balance = balance.subtract(e.getAmount());
                }
            }
        }
        return new QueryResultsContainer<BalancedEntry>(size, tmp);
    }

    /**
     * Set the number of entries before a balance checkpoint is made.
     * @param numberOfEntriesBeforeCheckpoint the checkpoint interval
     */
    public synchronized void setBalanceCheckpointInterval(int balanceCheckpointInterval) {
        this.balanceCheckpointInterval = balanceCheckpointInterval;
    }

    public synchronized int getBalanceCheckpointInterval() {
        return balanceCheckpointInterval;
    }

    public void setAsyncCheckpointCreation(boolean async) {
        log.info("setting async checkpoint creation to: {}", async);
        asyncCheckpointCreation.set(async);
    }

    @Override
    public boolean getAsyncCheckpointCreation() {
        return asyncCheckpointCreation.get();
    }

    public boolean isAsyncCheckpointCreation() {
        return asyncCheckpointCreation.get();
    }

    @Override
    public Transaction getTransactionById(Long txId) {
        return accountingDAO.getTransaction(txId);
    }

    @Override
    public CurrencyRate addCurrencyRate(CurrencyRate rate) {
        accountingDAO.saveOrUpdate(rate);
        return rate;
    }

    @Override
    public List<CurrencyRate> listRatesForCurrency(String currencyCode, Date date, int maxAgeDays) {

        // calculate dates
        DateMidnight dateMidnight = new DateTime(date).toDateMidnight();
        DateMidnight end = dateMidnight.plusDays(1);
        DateMidnight start = dateMidnight.minusDays(maxAgeDays);

        // list rates
        List<CurrencyRate> rates = accountingDAO.listCurrencyRates(currencyCode, start.toDate(), end.toDate());

        // remove duplicates
        Map<String, CurrencyRate> map = new HashMap<String, CurrencyRate>();
        for (CurrencyRate rate : rates) {
            String key = rate.getSourceCurrencyCode() + ":" + rate.getTargetCurrencyCode();
            map.put(key, rate);
        }

        // re-sort results
        List<CurrencyRate> answer = new ArrayList<CurrencyRate>(map.values());
        Collections.sort(rates, new Comparator<CurrencyRate>() {
            @Override
            public int compare(CurrencyRate o1, CurrencyRate o2) {
                int i = o1.getTimestamp().compareTo(o2.getTimestamp());
                if (i != 0) {
                    return i;
                } else {
                    return o1.getId().compareTo(o2.getId());
                }
            }
        });

        return answer;
    }

    @Override
    public CurrencyRate getCurrencyRate(String sourceCurrencyCode, String targetCurrencyCode, Date date,
            int fallbackDays) {
        return getCurrencyRate(sourceCurrencyCode, targetCurrencyCode, date, fallbackDays, true);
    }

    @Override
    public CurrencyRate getCurrencyRate(String sourceCurrencyCode, String targetCurrencyCode,
            String baslineCurrencyCode, Date date, int fallbackDays) {
        CurrencyRate rate = getCurrencyRate(sourceCurrencyCode, targetCurrencyCode, date, fallbackDays, false);
        if (rate != null) {
            // found a first level match, do return
            return rate;
        }
        // Now look for an exact chain with baseline in the middle
        CurrencyRate r1 = getCurrencyRate(sourceCurrencyCode, baslineCurrencyCode, date, fallbackDays, false);
        CurrencyRate r2 = getCurrencyRate(baslineCurrencyCode, targetCurrencyCode, date, fallbackDays, false);
        if (r1 == null || r2 == null) {
            // no direct chain, return null
            return null;
        }
        // now combine the rates
        rate = r1.combine(r2);
        return rate;
    }

    // --- PRIVATE METHODS --- //

    private void checkThrowNotExists(Account a, Long accountId) {
        if (a == null) {
            throw new AccountNotFoundException(accountId);
        }
    }

    private Transaction createTransactionInternal(String comment, String extId, List<Entry> entries,
            Map<String, String> atts) {

        // check that we indeed have the accounts
        ensureAccountsNotNull(entries);

        // create transaction (will check balance in constructor) and set attributes
        Transaction tx = new Transaction(comment, entries.toArray(new Entry[entries.size()]));
        if (atts != null) {
            tx.setStringAttributes(atts);
        }

        // set external id
        tx.setExternalId(extId);

        // save
        accountingDAO.saveOrUpdate(tx);

        // loop entries
        for (Entry entry : entries) {
            if (entry.getAccount() == null)
                throw new IllegalArgumentException("Entry " + entry.getId() + " does not contain an account");
            // check account status
            ensureAccountNotClosedAndExists(entry.getAccount());
            entry.setTransaction(tx);
            accountingDAO.saveOrUpdate(entry);
        }

        // check negative balances
        checkNegativeAccountBalances(entries);

        pluginManager.afterCreate(tx);

        return tx;
    }

    private BigDecimal calculateBalanceFromCheckpointToEntry(Long accountId, Long entryId,
            BalanceCheckpoint previousCP) {
        Long checkpointEntryId = -1l;
        BigDecimal checkpointBalance = BigDecimal.ZERO;
        if (previousCP != null) {
            checkpointBalance = previousCP.getBalance();
            checkpointEntryId = previousCP.getEntry().getId();
        }
        // sum checkpoint balance and all entries after it
        EntrySumAndCount entrySumAndCount = accountingDAO.getEntrySumAndCount(accountId, checkpointEntryId + 1,
                entryId);
        // update checkpoint count
        long count = entrySumAndCount.getCount();
        if (count > getBalanceCheckpointInterval()) {
            if (isAsyncCheckpointCreation()) {
                dirtyAccountSet.put(accountId, DUMMY_PAYLOAD);
            } else {
                doCreateCheckPoint(accountId, checkpointBalance, entrySumAndCount);
            }
        }
        BigDecimal balance = checkpointBalance.add(entrySumAndCount.getSum());
        return balance;
    }

    private void checkNegativeAccountBalances(List<Entry> entries) {
        for (Entry e : entries) {
            Account a = e.getAccount();
            if (!a.isNegativeBalanceAllowed() && e.getAmount().compareTo(BigDecimal.ZERO) < 0) {
                Money balance = getBalance(a.getId());
                BigDecimal amount = balance.getAmount();
                if (amount.signum() == -1) {
                    throw new NegativeBalanceException(a.getId());
                }
            }
        }
    }

    private void checkRequiredFields(Account account) {
        if (account.getCreated() == null) {
            account.setCreated(new Date());
        }
        if (account.getStatus() == null) {
            account.setStatus(AccountStatus.OPEN);
        }
    }

    private void ensureAccountNotClosedAndExists(Account a) {
        Account test = getAccount(a.getId());
        if (test == null)
            throw new AccountNotFoundException(a.getId());
        if (test.getStatus() != AccountStatus.OPEN) {
            if (!allowClosedAccounts.get()) {
                throw new AccountClosedException(a.getId());
            } else {
                log.warn("Allowing transaction involving closed account: " + a.getId());
            }
        }
    }

    private void ensureAccountsNotNull(Collection<Entry> entries) {
        for (Entry e : entries) {
            if (e.getAccount() == null) {
                throw new NullPointerException("account is null, entry = " + e.toString());
            }
        }
    }

    /**
     * Creates a balance checkpoint if the number of entries on the account
     * after the latest checkpoint is higher than a predefined value.
     * @param accountId the account to check
     * @param entrySumAndCount entry information
     * @param balance the new balance
     */
    private void createCheckpointIfNeeded(Long accountId) {
        BalanceCheckpoint latestCP = accountingDAO.getLatestBalanceCheckpoint(accountId, null);
        long latestEntryId = (latestCP == null ? -1L : latestCP.getEntry().getId());
        BigDecimal oldBalance = (latestCP == null ? BigDecimal.ZERO : latestCP.getBalance());
        EntrySumAndCount entrySumAndCount = accountingDAO.getEntrySumAndCount(accountId, latestEntryId + 1,
                Long.MAX_VALUE);
        doCreateCheckPoint(accountId, oldBalance, entrySumAndCount);
    }

    private void doCreateCheckPoint(Long accountId, BigDecimal oldBalance, EntrySumAndCount entrySumAndCount) {
        BigDecimal newBalance = oldBalance.add(entrySumAndCount.getSum());
        Long entryCount = entrySumAndCount.getCount();
        Long lastEntryId = entrySumAndCount.getLastEntryId();
        if (entryCount > getBalanceCheckpointInterval()) { // double check
            Entry e = accountingDAO.getEntry(lastEntryId);
            BalanceCheckpoint newCP = new BalanceCheckpoint(e, newBalance);
            accountingDAO.saveOrUpdate(newCP);
            log.debug("Created new balance checkpoint: aId = " + accountId + ", count = " + entryCount
                    + ", max eId = " + lastEntryId + ", balance = " + newBalance);
        } else {
            log.debug("Skipped balance checkpoint creation: aId = " + accountId + ", count = " + entryCount
                    + ", max eId = " + lastEntryId + ", balance = " + newBalance);
        }
    }

    private CurrencyRate getCurrencyRate(String sourceCurrencyCode, String targetCurrencyCode, Date date,
            int fallbackDays, boolean allowSearch) {
        DateMidnight dateMidnight = new DateTime(date).toDateMidnight();
        DateMidnight end = dateMidnight.plusDays(1);
        DateMidnight start = dateMidnight.minusDays(fallbackDays);

        CurrencyRate rate = accountingDAO.getCurrencyRate(sourceCurrencyCode, targetCurrencyCode, start.toDate(),
                end.toDate());
        if (rate == null) {

            // try opposite direction
            rate = accountingDAO.getCurrencyRate(targetCurrencyCode, sourceCurrencyCode, start.toDate(),
                    end.toDate());

            if (rate != null) {
                rate = rate.invert();
            }
        }

        if (rate == null && allowSearch) {
            // look for a first level chain
            List<CurrencyRate> chain = accountingDAO.getCurrencyRateChain(targetCurrencyCode, sourceCurrencyCode,
                    start.toDate(), end.toDate());

            if (!chain.isEmpty()) {
                // chain found, now we must put the rates in order and combine them
                CurrencyRate r0 = chain.get(0);
                CurrencyRate r1 = chain.get(1);

                CurrencyRate firstRate;
                CurrencyRate secondRate;

                if (r0.getSourceCurrencyCode().equals(sourceCurrencyCode)
                        || r0.getTargetCurrencyCode().equals(sourceCurrencyCode)) {
                    firstRate = r0;
                    secondRate = r1;
                } else {
                    firstRate = r1;
                    secondRate = r0;
                }

                if (!firstRate.getSourceCurrencyCode().equals(sourceCurrencyCode)) {
                    firstRate = firstRate.invert();
                }

                if (!secondRate.getTargetCurrencyCode().equals(targetCurrencyCode)) {
                    secondRate = secondRate.invert();
                }

                rate = firstRate.combine(secondRate);
            }
        }

        return rate;
    }

    private void checkThrowNotExists(Transaction t, Long transactionId) {
        if (t == null) {
            throw new TransactionNotFoundException(transactionId);
        }
    }
}