nl.strohalm.cyclos.services.accounts.AccountStatusHandlerImpl.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.services.accounts.AccountStatusHandlerImpl.java

Source

/*
   This file is part of Cyclos.
    
   Cyclos is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.
    
   Cyclos 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 General Public License for more details.
    
   You should have received a copy of the GNU General Public License
   along with Cyclos; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
    
 */
package nl.strohalm.cyclos.services.accounts;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import nl.strohalm.cyclos.dao.accounts.AccountDAO;
import nl.strohalm.cyclos.dao.accounts.AccountStatusDAO;
import nl.strohalm.cyclos.dao.accounts.PendingAccountStatusDAO;
import nl.strohalm.cyclos.dao.accounts.fee.account.AccountFeeChargeDAO;
import nl.strohalm.cyclos.dao.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.accounts.Account;
import nl.strohalm.cyclos.entities.accounts.AccountStatus;
import nl.strohalm.cyclos.entities.accounts.Currency;
import nl.strohalm.cyclos.entities.accounts.MemberAccount;
import nl.strohalm.cyclos.entities.accounts.MemberAccountStatus;
import nl.strohalm.cyclos.entities.accounts.PendingAccountStatus;
import nl.strohalm.cyclos.entities.accounts.SystemAccount;
import nl.strohalm.cyclos.entities.accounts.SystemAccountStatus;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFeeCharge;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFeeLog;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee.ChargeMode;
import nl.strohalm.cyclos.entities.accounts.transactions.Invoice;
import nl.strohalm.cyclos.entities.accounts.transactions.Payment;
import nl.strohalm.cyclos.entities.accounts.transactions.ScheduledPayment;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferAuthorization;
import nl.strohalm.cyclos.entities.accounts.transactions.Payment.Status;
import nl.strohalm.cyclos.entities.alerts.SystemAlert;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.services.accountfees.AccountFeeService;
import nl.strohalm.cyclos.services.accounts.rates.ARateService;
import nl.strohalm.cyclos.services.accounts.rates.DRateService;
import nl.strohalm.cyclos.services.alerts.AlertService;
import nl.strohalm.cyclos.services.fetch.FetchService;
import nl.strohalm.cyclos.services.settings.SettingsService;
import nl.strohalm.cyclos.utils.CurrentTransactionData;
import nl.strohalm.cyclos.utils.DataIteratorHelper;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.CurrentTransactionData.Entry;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.conversion.CalendarConverter;
import nl.strohalm.cyclos.utils.conversion.UnitsConverter;
import nl.strohalm.cyclos.utils.validation.ValidationException;

import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

/**
 * Handles the updating of account status
 * @author luis
 * @author Rinke (all dealing with rates)
 */
public class AccountStatusHandlerImpl implements AccountStatusHandler, Runnable, InitializingBean, DisposableBean {

    private static enum RunStatus {
        SUCCESS, ERROR, RETRY, EMPTY
    }

    private static final float PRECISION_DELTA = 0.0001F;

    // set MAX_PROCESSING_BATCH to 1 to avoid potential deadlocks.
    private static final int MAX_PROCESSING_BATCH = 1;
    private static final int MAX_ERROR_ATTEMPTS = 6;
    private BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
    private PendingAccountStatusDAO pendingAccountStatusDao;
    private TransactionTemplate transactionTemplate;
    private Thread thread;
    private AccountStatusDAO accountStatusDao;
    private AccountFeeChargeDAO accountFeeChargeDao;
    private AccountFeeService accountFeeService;
    private SettingsService settingsService;
    private AlertService alertService;
    private FetchService fetchService;
    private DRateService dRateService;
    private ARateService aRateService;
    private boolean inProcess;
    private int errors;
    private AccountDAO accountDao;

    public void afterPropertiesSet() throws Exception {
        thread = new Thread(this, "AccountStatusHandler");
        thread.setPriority(Thread.MAX_PRIORITY);
        thread.start();
    }

    public void destroy() throws Exception {
        stopThread();
    }

    public AccountStatus getStatus(Account account, final Calendar date, final boolean processCurrentStatus,
            final boolean processAccountFeePeriod) {
        AccountStatus status;
        lock(false, account);
        try {
            status = accountStatusDao.getByDate(account, date);
        } catch (final EntityNotFoundException e) {
            // When no account status is found, create a new one with values = zero
            account = fetchService.fetch(account);
            if (account instanceof MemberAccount) {
                status = new MemberAccountStatus((MemberAccount) account);
            } else {
                status = new SystemAccountStatus((SystemAccount) account);
            }
        }

        if (status instanceof MemberAccountStatus) {
            MemberAccountStatus memberStatus = (MemberAccountStatus) status;

            // When the account fee period should be processed, ensure a new AccountStatus is created on the period end
            if (processAccountFeePeriod) {
                final Collection<AccountFeeCharge> charges = accountFeeService
                        .insertMissingChargesForPreviousPeriods(memberStatus, date);
                if (CollectionUtils.isNotEmpty(charges)) {
                    for (final AccountFeeCharge charge : charges) {
                        final MemberAccountStatus newStatus = memberStatus.newBasedOnThis();
                        newStatus.setDate(charge.getPeriod().getEnd());
                        newStatus.setAccountFeeLog(charge.getAccountFeeLog());
                        newStatus.setVolumeAccountFees(newStatus.getVolumeAccountFees().add(charge.getAmount()));
                        // Insert the status, updating the memberStatus instance
                        memberStatus = accountStatusDao.insert(newStatus);
                    }
                }
            }

            // For member accounts, get the updated reserved amount for volume account fees
            if (processCurrentStatus) {
                status = accountFeeService.adjustVolumeChargesToDate(memberStatus, date);
            }
        }

        // If the current status should be processed, also take into account the delta by pending account statuses
        if (processCurrentStatus) {
            final Iterator<PendingAccountStatus> iterator = pendingAccountStatusDao.iterateFor(account);
            try {
                if (iterator.hasNext()) {
                    while (iterator.hasNext()) {
                        final PendingAccountStatus pendingStatus = iterator.next();
                        switch (pendingStatus.getType()) {
                        case PAYMENT:
                            final Transfer transfer = fetchService.fetch(pendingStatus.getTransfer(),
                                    Transfer.Relationships.FROM, Transfer.Relationships.TO);
                            if (shouldUpdateToAccountOnPayment(pendingStatus, transfer)
                                    && transfer.getTo().equals(account)) {
                                status = updateAccountStatusOnPayment(pendingStatus, true, transfer, status);
                            }
                            if (shouldUpdateFromAccountOnPayment(pendingStatus, transfer)
                                    && transfer.getFrom().equals(account)) {
                                status = updateAccountStatusOnPayment(pendingStatus, false, transfer, status);
                            }
                            break;
                        case RESERVED_SCHEDULED_PAYMENT:
                            status = updateAccountStatusOnReservedScheduledPayment(pendingStatus, status, false);
                            break;
                        case LIBERATE_RESERVED_INSTALLMENT:
                            status = updateAccountStatusOnLiberateReservedInstallment(pendingStatus, status, false);
                            break;
                        case ACCOUNT_FEE_DISABLED:
                            status = updateAccountStatusAccountFeeDisabled(pendingStatus,
                                    (MemberAccountStatus) status, false);
                            break;
                        case ACCOUNT_FEE_INVOICE:
                            status = updateAccountStatusOnAccountFeeInvoice(pendingStatus,
                                    (MemberAccountStatus) status, false);
                            break;
                        case AUTHORIZATION:
                            status = updateAccountStatusOnAuthorization(pendingStatus, status, false);
                            break;
                        case LIMIT_CHANGE:
                            status = updateAccountStatusOnCreditLimitChange(pendingStatus, status, false);
                            break;
                        default:
                            System.out.println("Invalid pending account status type: " + pendingStatus.getType());
                            break;
                        }
                    }
                }
            } finally {
                DataIteratorHelper.close(iterator);
            }
        }
        return status;
    }

    public void initialize() {
        final int count = pendingAccountStatusDao.count();
        processNext(count);
    }

    public PendingAccountStatus liberateReservedAmountForInstallment(final Transfer transfer) {
        ensureAlive();
        final ScheduledPayment scheduledPayment = transfer.getScheduledPayment();
        if (scheduledPayment == null || !scheduledPayment.isReserveAmount()) {
            return null;
        }
        // Insert the pending Status
        PendingAccountStatus pendingStatus = new PendingAccountStatus();
        pendingStatus.setType(PendingAccountStatus.Type.LIBERATE_RESERVED_INSTALLMENT);
        pendingStatus.setDate(scheduledPayment.getDate());
        pendingStatus.setScheduledPayment(scheduledPayment);
        pendingStatus.setAccount(scheduledPayment.getFrom());
        pendingStatus.setTransfer(transfer);
        pendingStatus.setTransferStatus(transfer.getStatus());
        pendingStatus = pendingAccountStatusDao.insert(pendingStatus);
        CurrentTransactionData.addPendingAccountStatus();
        return pendingStatus;
    }

    /**
     * Creates a PendingAccountStatus for an account fee disabled, which should affect the given account
     */
    public PendingAccountStatus processAccountFeeDisabled(final MemberAccount account,
            final BigDecimal subtractedAmount) {
        ensureAlive();
        PendingAccountStatus pendingStatus = new PendingAccountStatus();
        pendingStatus.setType(PendingAccountStatus.Type.ACCOUNT_FEE_DISABLED);
        pendingStatus.setDate(Calendar.getInstance());
        pendingStatus.setAccount(account);
        pendingStatus.setSubtractedAmount(subtractedAmount);
        pendingStatus = pendingAccountStatusDao.insert(pendingStatus);
        CurrentTransactionData.addPendingAccountStatus();
        return pendingStatus;
    }

    /**
     * Creates a PendingAccountStatus for an account fee disabled, which should affect the given account
     */
    public PendingAccountStatus processAccountFeeInvoice(final Account account, final Invoice invoice) {
        ensureAlive();
        PendingAccountStatus pendingStatus = new PendingAccountStatus();
        pendingStatus.setType(PendingAccountStatus.Type.ACCOUNT_FEE_INVOICE);
        pendingStatus.setDate(Calendar.getInstance());
        pendingStatus.setAccount(account);
        pendingStatus.setInvoice(invoice);
        pendingStatus = pendingAccountStatusDao.insert(pendingStatus);
        CurrentTransactionData.addPendingAccountStatus();
        return pendingStatus;
    }

    /**
     * Creates a PendingAccountStatus for a transfer authorization <br>
     * <b>IMPORTANT</b>: Make sure that this is first called on the to account, then on the from account. Otherwise rate calculations work with the
     * wrong balance. See remark on updateAccounStatusonPayment.
     * 
     */
    public PendingAccountStatus processAuthorization(final Account account, final Transfer transfer,
            final TransferAuthorization transferAuthorization) {
        ensureAlive();
        PendingAccountStatus pendingStatus = new PendingAccountStatus();
        pendingStatus.setType(PendingAccountStatus.Type.AUTHORIZATION);
        pendingStatus.setDate(Calendar.getInstance());
        pendingStatus.setAccount(account);
        pendingStatus.setTransfer(transfer);
        pendingStatus.setTransferStatus(transfer.getStatus());
        pendingStatus.setTransferAuthorization(transferAuthorization);
        pendingStatus = pendingAccountStatusDao.insert(pendingStatus);
        CurrentTransactionData.addPendingAccountStatus();
        return pendingStatus;
    }

    public void processFromCurrentTransaction() {
        final Entry entry = CurrentTransactionData.getEntry();
        final int pendingAccountStatuses = entry == null ? 0 : entry.getPendingAccountStatuses();
        if (pendingAccountStatuses > 0) {
            processNext(pendingAccountStatuses);
        }
    }

    public PendingAccountStatus processLimitChange(final Account account) {
        ensureAlive();
        PendingAccountStatus pendingStatus = new PendingAccountStatus();
        pendingStatus.setType(PendingAccountStatus.Type.LIMIT_CHANGE);
        pendingStatus.setDate(Calendar.getInstance());
        pendingStatus.setAccount(account);
        pendingStatus.setLowerLimit(account.getCreditLimit());
        pendingStatus.setUpperLimit(account.getUpperCreditLimit());
        if (LoggedUser.isValid()) {
            pendingStatus.setBy(LoggedUser.element());
        }
        pendingStatus = pendingAccountStatusDao.insert(pendingStatus);
        CurrentTransactionData.addPendingAccountStatus();
        return pendingStatus;
    }

    public void processNext(final int count) {
        // Ensure a max count is offered each time
        int current = count;
        while (current > 0) {
            queue.offer(Math.min(MAX_PROCESSING_BATCH, current));
            current -= MAX_PROCESSING_BATCH;
        }
    }

    public PendingAccountStatus processReservedScheduledPayment(final ScheduledPayment scheduledPayment) {
        ensureAlive();
        if (!scheduledPayment.isReserveAmount()) {
            throw new IllegalArgumentException("Scheduled payment is not set to reserve the amount");
        }
        // Insert the pending Status
        PendingAccountStatus pendingStatus = new PendingAccountStatus();
        pendingStatus.setType(PendingAccountStatus.Type.RESERVED_SCHEDULED_PAYMENT);
        pendingStatus.setDate(scheduledPayment.getDate());
        pendingStatus.setScheduledPayment(scheduledPayment);
        pendingStatus = pendingAccountStatusDao.insert(pendingStatus);
        CurrentTransactionData.addPendingAccountStatus();
        return pendingStatus;
    }

    public PendingAccountStatus processTransfer(final Transfer transfer) {
        ensureAlive();
        // Insert the pending Status
        PendingAccountStatus pendingStatus = new PendingAccountStatus();
        pendingStatus.setType(PendingAccountStatus.Type.PAYMENT);
        pendingStatus.setDate(transfer.getDate());
        pendingStatus.setTransfer(transfer);
        pendingStatus.setTransferStatus(transfer.getStatus());
        pendingStatus = pendingAccountStatusDao.insert(pendingStatus);
        CurrentTransactionData.addPendingAccountStatus();
        return pendingStatus;
    }

    public void run() {
        try {
            while (true) {
                // Wait until something is available
                final Integer count = queue.take();
                if (count != null && count > 0) {
                    processQueue(count);
                }
            }
        } catch (final InterruptedException e) {
            // Ignore
        }
    }

    public void setAccountDao(final AccountDAO accountDao) {
        this.accountDao = accountDao;
    }

    public void setAccountFeeChargeDao(final AccountFeeChargeDAO accountFeeChargeDao) {
        this.accountFeeChargeDao = accountFeeChargeDao;
    }

    public void setAccountFeeService(final AccountFeeService accountFeeService) {
        this.accountFeeService = accountFeeService;
    }

    public void setAccountStatusDao(final AccountStatusDAO accountStatusDao) {
        this.accountStatusDao = accountStatusDao;
    }

    public void setAlertService(final AlertService alertService) {
        this.alertService = alertService;
    }

    public void setaRateService(final ARateService aRateService) {
        this.aRateService = aRateService;
    }

    public void setdRateService(final DRateService dRateService) {
        this.dRateService = dRateService;
    }

    public void setFetchService(final FetchService fetchService) {
        this.fetchService = fetchService;
    }

    public void setPendingAccountStatusDao(final PendingAccountStatusDAO pendingAccountStatusDao) {
        this.pendingAccountStatusDao = pendingAccountStatusDao;
    }

    public void setSettingsService(final SettingsService settingsService) {
        this.settingsService = settingsService;
    }

    public void setTransactionTemplate(final TransactionTemplate transactionTemplate) {
        this.transactionTemplate = transactionTemplate;
    }

    public void waitUntilProcessAll() {
        while (!queue.isEmpty() || inProcess) {
            try {
                Thread.sleep(500);
            } catch (final InterruptedException e) {
                return;
            }
        }
    }

    private void calculateVolumeCharges(final AccountStatus last, final AccountStatus current) {
        if (current instanceof MemberAccountStatus) {
            final MemberAccountStatus memberStatus = (MemberAccountStatus) current;
            final Collection<AccountFeeCharge> charges = accountFeeService
                    .calculateVolumeCharges((MemberAccountStatus) last, memberStatus.getDate());
            for (final AccountFeeCharge charge : charges) {
                if (charge.getAmount().floatValue() > PRECISION_DELTA) {
                    memberStatus.setVolumeAccountFees(memberStatus.getVolumeAccountFees().add(charge.getAmount()));
                    accountFeeChargeDao.insert(charge);
                }
            }
        }
    }

    private void ensureAlive() {
        if (thread == null) {
            throw new ValidationException("general.error.accountStatusProcessing");
        }
    }

    private boolean lock(final boolean forWrite, final Account... accounts) {
        try {
            accountDao.lock(forWrite, Arrays.asList(accounts));
            return true;
        } catch (final Exception e) {
            return false;
        }
    }

    /**
     * Processes the next n pending account statuses. Warning: if passing more than 1, on high concurrent systems, it's likely to cause deadlocks, as
     * accounts are locked in the database.
     * @return Returns whether something was actually processed - false if there were no pending records to process
     */
    private boolean processQueue(final int count) throws InterruptedException {
        inProcess = true;
        final RunStatus status = transactionTemplate.execute(new TransactionCallback<RunStatus>() {
            public RunStatus doInTransaction(final TransactionStatus status) {
                return runInTransaction(status, count);
            }
        });
        CurrentTransactionData.cleanup();
        switch (status) {
        case RETRY:
            // Just retry the same pending account status after a small sleep
            queue.offer(count);
            break;
        case ERROR:
            errors++;
            if (errors <= MAX_ERROR_ATTEMPTS) {
                // Sleep 2sec, then 4, then 8, then 16...
                Thread.sleep((long) (Math.pow(2, errors) * 1000));
                // Ensure there will be another try for this status
                queue.offer(count);
            } else {
                // CRITICAL STATE!!!
                // We've not managed to resolve the error, and no more account status processing is done, which means
                // no more payments can be done! Kill the thread and raise an alert
                stopThread();
                transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                    @Override
                    protected void doInTransactionWithoutResult(final TransactionStatus status) {
                        PendingAccountStatus pendingStatus = null;
                        Object[] arguments = {};
                        try {
                            pendingStatus = pendingAccountStatusDao.next(1).get(0);
                            final Transfer transfer = pendingStatus.getTransfer();
                            if (transfer != null) {
                                final LocalSettings localSettings = settingsService.getLocalSettings();
                                final UnitsConverter unitsConverter = localSettings
                                        .getUnitsConverter(transfer.getType().getFrom().getCurrency().getPattern());
                                final CalendarConverter dateConverter = localSettings.getDateConverter();
                                arguments = new Object[] { dateConverter.toString(pendingStatus.getDate()),
                                        transfer.getFrom().getOwnerName(), transfer.getTo().getOwnerName(),
                                        unitsConverter.toString(transfer.getAmount()) };
                            }
                        } catch (final Exception e) {
                            // Ok, leave null
                        }
                        alertService.create(SystemAlert.Alerts.ERROR_PROCESSING_ACCOUNT_STATUS, arguments);
                    }
                });
            }
        }
        inProcess = false;
        return status != RunStatus.EMPTY;
    }

    private RunStatus runInTransaction(final TransactionStatus status, final int count) {
        try {
            final List<PendingAccountStatus> pendings = pendingAccountStatusDao.next(count);
            if (pendings.isEmpty()) {
                return RunStatus.EMPTY;
            }
            for (final PendingAccountStatus pendingStatus : pendings) {
                final Account account = pendingStatus.getAccount();
                if (account != null) {
                    // In cases where the pending account status references the account (ACCOUNT_FEE_DISABLED, ACCOUNT_FEE_INVOICE, AUTHORIZATION
                    // and LIMIT_CHANGE), perform the lock outside the switch to avoid code replication
                    if (!lock(true, account)) {
                        status.setRollbackOnly();
                        return RunStatus.RETRY;
                    }
                }
                switch (pendingStatus.getType()) {
                case PAYMENT:
                    final Transfer transfer = pendingStatus.getTransfer();
                    final boolean updateTo = shouldUpdateToAccountOnPayment(pendingStatus, transfer);
                    final boolean updateFrom = shouldUpdateFromAccountOnPayment(pendingStatus, transfer);

                    // Lock the required accounts
                    Account[] toLock = null;
                    if (updateFrom && updateTo) {
                        toLock = new Account[] { transfer.getFrom(), transfer.getTo() };
                    } else if (updateFrom) {
                        toLock = new Account[] { transfer.getFrom() };
                    } else if (updateTo) {
                        toLock = new Account[] { transfer.getTo() };
                    }
                    if (toLock != null) {
                        if (!lock(true, toLock)) {
                            status.setRollbackOnly();
                            return RunStatus.RETRY;
                        }
                    }

                    // IMPORTANT: The to-account status must be updated before the from-account status
                    if (updateTo) {
                        updateAccountStatusOnPayment(pendingStatus, true, transfer, null);
                    }
                    if (updateFrom) {
                        updateAccountStatusOnPayment(pendingStatus, false, transfer, null);
                    }
                    break;
                case RESERVED_SCHEDULED_PAYMENT:
                    // Lock the from account, to ensure the available balance is ok
                    if (!lock(true, pendingStatus.getScheduledPayment().getFrom())) {
                        status.setRollbackOnly();
                        return RunStatus.RETRY;
                    }
                    updateAccountStatusOnReservedScheduledPayment(pendingStatus, null, true);
                    break;
                case LIBERATE_RESERVED_INSTALLMENT:
                    updateAccountStatusOnLiberateReservedInstallment(pendingStatus, null, true);
                    break;
                case ACCOUNT_FEE_DISABLED:
                    updateAccountStatusAccountFeeDisabled(pendingStatus, null, true);
                    break;
                case ACCOUNT_FEE_INVOICE:
                    updateAccountStatusOnAccountFeeInvoice(pendingStatus, null, true);
                    break;
                case AUTHORIZATION:
                    updateAccountStatusOnAuthorization(pendingStatus, null, true);
                    break;
                case LIMIT_CHANGE:
                    updateAccountStatusOnCreditLimitChange(pendingStatus, null, true);
                    break;
                default:
                    System.out.println("Invalid pending account status type: " + pendingStatus.getType());
                    break;
                }
                // Remove the pending status after processing the new account status
                pendingAccountStatusDao.delete(pendingStatus.getId());
                errors = 0;
            }
            return RunStatus.SUCCESS;

        } catch (final Exception e) {
            e.printStackTrace();
            status.setRollbackOnly();
            return RunStatus.ERROR;
        }
    }

    private boolean shouldUpdateFromAccountOnPayment(final PendingAccountStatus pendingAccountStatus,
            final Transfer transfer) {
        /*
         * On pending debits, generated fees should not update the account status, or there would be a reserved amount for pending debits that would
         * only be generated when the main transfer were authorized. This is, however, distinct from when the same account is paying a generated
         * transfer (such as a conversion fee), because both amounts will be charged from the same account at once
         */
        final Transfer parent = transfer.getParent();
        final boolean parentAndChildFromSameAccount = parent != null && parent.getFrom().equals(transfer.getFrom());
        final boolean pending = (parent != null
                && pendingAccountStatus.getTransferStatus() == Payment.Status.PENDING);
        return parentAndChildFromSameAccount || !pending;
    }

    private boolean shouldUpdateToAccountOnPayment(final PendingAccountStatus pendingAccountStatus,
            final Transfer transfer) {
        // Should update the to account status only if the transfer is processed
        return pendingAccountStatus.getTransferStatus() == Payment.Status.PROCESSED;
    }

    private void stopThread() {
        if (thread != null) {
            thread.interrupt();
            thread = null;
        }
    }

    private MemberAccountStatus updateAccountStatusAccountFeeDisabled(
            final PendingAccountStatus pendingAccountStatus, MemberAccountStatus last, final boolean insert) {
        final Account account = pendingAccountStatus.getAccount();
        final BigDecimal amount = pendingAccountStatus.getSubtractedAmount();
        if (last == null) {
            last = (MemberAccountStatus) getStatus(account, null, false, false);
        }
        MemberAccountStatus status = last.newBasedOnThis();
        status.setVolumeAccountFees(status.getVolumeAccountFees().subtract(amount));
        if (insert) {
            status = accountStatusDao.insert(status, false);
        }
        return status;
    }

    private AccountStatus updateAccountStatusOnAccountFeeInvoice(final PendingAccountStatus pendingAccountStatus,
            MemberAccountStatus last, final boolean insert) {
        final Account account = pendingAccountStatus.getAccount();
        final Invoice invoice = fetchService.fetch(pendingAccountStatus.getInvoice(), RelationshipHelper
                .nested(Invoice.Relationships.ACCOUNT_FEE_LOG, AccountFeeLog.Relationships.ACCOUNT_FEE));
        final AccountFeeLog accountFeeLog = invoice.getAccountFeeLog();
        if (accountFeeLog != null) {
            final ChargeMode chargeMode = accountFeeLog.getAccountFee().getChargeMode();
            if (chargeMode.isVolume()) {
                if (last == null) {
                    last = (MemberAccountStatus) getStatus(account, null, false, true);
                }
                final AccountStatus status = last.newBasedOnThis();
                MemberAccountStatus memberStatus = (MemberAccountStatus) status;
                final BigDecimal totalFeeAmount = accountFeeChargeDao.totalAmoutForPeriod((MemberAccount) account,
                        accountFeeLog);
                memberStatus.setVolumeAccountFees(memberStatus.getVolumeAccountFees().subtract(totalFeeAmount));
                // Save the status
                if (insert) {
                    memberStatus = (MemberAccountStatus) accountStatusDao.insert(status, false);
                }
                return memberStatus;
            }
        }
        return last;
    }

    /**
     * Beware the order in which this is called. First on the to account, then on the from account. Otherwise rate calculations work with the wrong
     * balance. See remark on updateAccounStatusonPayment.
     * 
     */
    private AccountStatus updateAccountStatusOnAuthorization(final PendingAccountStatus pendingAccountStatus,
            AccountStatus last, final boolean insert) {
        final Account account = pendingAccountStatus.getAccount();
        final Transfer transfer = pendingAccountStatus.getTransfer();
        final TransferAuthorization transferAuthorization = pendingAccountStatus.getTransferAuthorization();

        if (last == null) {
            last = getStatus(account, null, false, true);
        }
        AccountStatus status = last.newBasedOnThis();
        final boolean isDebit = account.equals(transfer.getFrom());
        final boolean isAuthorized = pendingAccountStatus.getTransferStatus() == Payment.Status.PROCESSED;
        final BigDecimal amount = transfer.getAmount();

        // update balance
        if (isAuthorized) {
            // A PendingAccountStatus for authorization is only inserted when the last level has been reached (the payment is processed).
            // So, we don't have to worry here that an authorization action has been done but the payment is not yet processed.

            // APPLY RATES (d-rate, a-rate)
            updateRatesOnAccountStatus(status, transfer);

            // Update the balance
            updateBalanceWithTransfer(pendingAccountStatus, status, transfer);
        }
        if (isDebit) {
            status.setPendingDebits(status.getPendingDebits().subtract(1, amount));
        }
        status.setTransferAuthorization(transferAuthorization);

        // For member accounts, we must also calculate the account fee charge
        calculateVolumeCharges(last, status);

        // Save the status
        if (insert) {
            status = accountStatusDao.insert(status, false);
        }
        return status;
    }

    private AccountStatus updateAccountStatusOnCreditLimitChange(final PendingAccountStatus pendingAccountStatus,
            AccountStatus last, final boolean insert) {
        final Account account = pendingAccountStatus.getAccount();

        // Update the account status
        if (last == null) {
            last = getStatus(account, null, false, true);
        }
        AccountStatus status = last.newBasedOnThis();
        status.setCreditLimit(pendingAccountStatus.getLowerLimit());
        status.setUpperCreditLimit(pendingAccountStatus.getUpperLimit());
        status.setCreditLimitChangedBy(pendingAccountStatus.getBy());

        // For member accounts, we must also calculate the account fee charge
        calculateVolumeCharges(last, status);

        // Save the status
        if (insert) {
            status = accountStatusDao.insert(status, false);
        }
        return status;
    }

    private AccountStatus updateAccountStatusOnLiberateReservedInstallment(
            final PendingAccountStatus pendingAccountStatus, AccountStatus last, final boolean insert) {
        final Account account = pendingAccountStatus.getAccount();

        // Update the account status
        if (last == null) {
            last = getStatus(account, null, false, true);
        }
        AccountStatus status = last.newBasedOnThis();
        final Transfer transfer = pendingAccountStatus.getTransfer();
        final ScheduledPayment scheduledPayment = transfer.getScheduledPayment();
        status.setTransfer(transfer);
        status.setScheduledPayment(scheduledPayment);
        status.setReservedScheduledPayments(
                status.getReservedScheduledPayments().subtract(transfer.getActualAmount()));

        // Save the status
        if (insert) {
            status = accountStatusDao.insert(status, false);
        }
        return status;
    }

    /**
     * <b>Very important:</b> The order in which methods are called for a transfer is very important for the correct handling of rates.<br>
     * This order should be as follows:
     * <ol>
     * <li>First handle the complete procedure for the to account. This is because the balance and rateBalanceCorrection of the from account are
     * needed for correctly passing rates to the to-account. If you would first update the accountStatus of the from account, then balance and RBC of
     * the from account are already changed, meaning that the next call for the to-account would use the wrong values for passing the rates.
     * <li>when changing an account (to or from), first take care that the rate fields are initialized. Null rate fields will result in repeated alert
     * fires.
     * <li>First apply rates before changing the account balances, otherwise the rates methods will work with the wrong account balance.
     * <li>Only then update the balance. Use AccountStatusDAO.updateBalanceWithTransfer for this, as it will also correct the RateBalanceCorrection
     * Field.
     * </ol>
     */
    private AccountStatus updateAccountStatusOnPayment(final PendingAccountStatus pendingStatus, final boolean to,
            final Transfer transfer, AccountStatus last) {
        final Account account = to ? transfer.getTo() : transfer.getFrom();
        final boolean from = !to;

        final Status transferStatus = pendingStatus.getTransferStatus();
        final boolean isProcessed = transferStatus == Payment.Status.PROCESSED;
        if (!isProcessed && transferStatus != Payment.Status.PENDING) {
            // When it's neither processed nor pending, we won't generate a new account status
            return last;
        }

        final boolean insert = last == null;

        // A very special case here is an scheduled payment which is manually processed (pay now) and still has to be authorized.
        // In this case, the process date is null and the date is on the future. We can't use either, but the current date instead
        final Calendar date = transfer.getProcessDate() == null ? Calendar.getInstance()
                : transfer.getProcessDate();
        final Currency currency = account.getType().getCurrency();
        if (last == null) {
            if (isProcessed && (currency.isEnableARate() || currency.isEnableDRate())) {
                // for rates, the accountstatus MUST be the last one available up to present, because updating next statuses gets really complicated
                last = getStatus(account, null, false, false);
            } else {
                last = getStatus(account, date, false, false);
            }
        }
        AccountStatus status = last.newBasedOnThis();

        // For payments that are not on the past date, update the amount for the new one
        final boolean isDebit = account.equals(transfer.getFrom());
        final BigDecimal amount = transfer.getAmount();

        if (isProcessed) {
            // APPLY RATES (d-rate, a-rate)
            updateRatesOnAccountStatus(status, transfer);

            // update balance
            updateBalanceWithTransfer(pendingStatus, status, transfer);
        } else if (isDebit) {
            // Update only the pending debits
            status.setPendingDebits(status.getPendingDebits().add(amount));
        }

        // Process the reserved amount for total amount on scheduled payments, if applicable
        if (from && transfer.getScheduledPayment() != null && transfer.getScheduledPayment().isReserveAmount()) {
            status.setReservedScheduledPayments(
                    status.getReservedScheduledPayments().subtract(transfer.getAmount()));
        }

        // For member accounts:
        if (status instanceof MemberAccountStatus) {
            final MemberAccountStatus memberStatus = (MemberAccountStatus) status;

            // On top-level payments of account fees, reduce the account fees accumulator
            boolean isVolumeAccountFee = false;
            final AccountFeeLog accountFeeLog = transfer.getAccountFeeLog();
            if (transfer.getParent() == null && accountFeeLog != null) {
                isVolumeAccountFee = accountFeeLog.getAccountFee().getChargeMode().isVolume();
            }

            if (isVolumeAccountFee) {
                // Subtract the account fees when this transfer is related to a volume account fee
                final BigDecimal totalFeeAmount = accountFeeChargeDao.totalAmoutForPeriod((MemberAccount) account,
                        transfer.getAccountFeeLog());
                memberStatus.setVolumeAccountFees(memberStatus.getVolumeAccountFees().subtract(totalFeeAmount));
            } else {
                // Calculate the account fee charges
                calculateVolumeCharges(last, status);
            }
        }

        // Save the status
        status.setTransfer(transfer);
        status.setDate(date);
        if (insert) {
            status = accountStatusDao.insert(status, false);

            // Ensure the account status in future, if exists, are ok
            if (!currency.isEnableARate() && !currency.isEnableDRate()) {
                accountStatusDao.updateStatusesInFuture(status);
            }
        }
        return status;
    }

    private AccountStatus updateAccountStatusOnReservedScheduledPayment(final PendingAccountStatus pendingStatus,
            AccountStatus last, final boolean insert) {
        final ScheduledPayment scheduledPayment = pendingStatus.getScheduledPayment();
        final Calendar date = scheduledPayment.getDate();
        final Account account = scheduledPayment.getFrom();
        if (last == null) {
            last = getStatus(account, date, false, false);
        }

        // Calculate what will be added
        final int payments = scheduledPayment.getTransfers().size();
        BigDecimal totalAmount = BigDecimal.ZERO;
        for (final Transfer transfer : scheduledPayment.getTransfers()) {
            totalAmount = totalAmount.add(transfer.getAmount());
        }

        // Create the new status
        AccountStatus status = last.newBasedOnThis();
        status.setDate(date);
        status.setScheduledPayment(scheduledPayment);
        status.setReservedScheduledPayments(status.getReservedScheduledPayments().add(payments, totalAmount));
        if (insert) {
            status = accountStatusDao.insert(status, false);
        }

        return status;
    }

    /**
     * adds or substracts an amount to the status balance. Call this in stead of directly increasing credits or debits, as it takes care for rate
     * balance correction field too. Only updates the real credits and debits, not the pending debits and credits.
     * 
     * @param status The status to be updated. Nothing is saved; the status fields are just updated.
     * @param transfer the transfer being processed.
     */
    private void updateBalanceWithTransfer(final PendingAccountStatus pendingStatus, final AccountStatus status,
            final Transfer transfer) {
        if (pendingStatus.getTransferStatus() != Payment.Status.PROCESSED) {
            // method only deals with real debits and credits; not with pendings...
            return;
        }
        final BigDecimal amount = transfer.getAmount();
        final boolean isDebit = status.getAccount().equals(transfer.getFrom());
        final boolean isRoot = transfer.isRoot();

        final Currency currency = status.getAccount().getType().getCurrency();
        final Calendar date = transfer.getProcessDate();
        final boolean rated = (currency.isEnableARate(date) || currency.isEnableDRate(date));
        if (isDebit) {
            // call this first, before the balance is updated.
            if (rated) {
                aRateService.updateRateBalanceCorrectionOnFromAccount(status, amount);
            }
            if (isRoot) {
                status.setRootDebits(status.getRootDebits().add(amount));
            } else {
                status.setNestedDebits(status.getNestedDebits().add(amount));
            }
        } else {
            // call this first, before the balance is updated.
            if (rated && amount.compareTo(BigDecimal.ZERO) < 0) {
                // special case for chargebacks (having negative transfer amounts)
                aRateService.updateRateBalanceCorrectionOnFromAccount(status, amount.negate());
            }
            if (isRoot) {
                status.setRootCredits(status.getRootCredits().add(amount));
            } else {
                status.setNestedCredits(status.getNestedCredits().add(amount));
            }
        }
    }

    private void updateRatesOnAccountStatus(AccountStatus status, final Transfer transfer) {
        final Calendar date = transfer.getProcessDate() == null ? transfer.getDate() : transfer.getProcessDate();
        final Currency currency = status.getAccount().getType().getCurrency();
        if (currency.isEnableARate(date) || currency.isEnableDRate(date)) {
            status = aRateService.initializeRateBalanceCorrectionOnAccounts(status, transfer);
        }
        final boolean isDebit = status.getAccount().equals(transfer.getFrom());
        if (!isDebit) {
            aRateService.applyTransfer(status, transfer);
            dRateService.applyTransfer(status, transfer);
        }
    }

}