nl.strohalm.cyclos.services.accountfees.AccountFeeServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.services.accountfees.AccountFeeServiceImpl.java

Source

/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
    
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.accountfees;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import nl.strohalm.cyclos.dao.accounts.AccountDAO;
import nl.strohalm.cyclos.dao.accounts.AccountDailyDifference;
import nl.strohalm.cyclos.dao.accounts.fee.account.AccountFeeDAO;
import nl.strohalm.cyclos.dao.accounts.fee.account.AccountFeeLogDAO;
import nl.strohalm.cyclos.dao.accounts.fee.account.MemberAccountFeeLogDAO;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.accounts.AccountStatus;
import nl.strohalm.cyclos.entities.accounts.AccountType;
import nl.strohalm.cyclos.entities.accounts.MemberAccount;
import nl.strohalm.cyclos.entities.accounts.MemberAccountType;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee.ChargeMode;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee.InvoiceMode;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee.PaymentDirection;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFee.RunMode;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFeeLog;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFeeLogDetailsDTO;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFeeLogQuery;
import nl.strohalm.cyclos.entities.accounts.fees.account.AccountFeeQuery;
import nl.strohalm.cyclos.entities.accounts.fees.account.MemberAccountFeeLog;
import nl.strohalm.cyclos.entities.accounts.fees.account.MemberAccountFeeLogQuery;
import nl.strohalm.cyclos.entities.accounts.transactions.Invoice;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.scheduling.polling.ChargeAccountFeePollingTask;
import nl.strohalm.cyclos.services.InitializingService;
import nl.strohalm.cyclos.services.accounts.AccountDTO;
import nl.strohalm.cyclos.services.accounts.AccountDateDTO;
import nl.strohalm.cyclos.services.accounts.AccountServiceLocal;
import nl.strohalm.cyclos.services.application.ApplicationServiceLocal;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.services.transactions.PaymentServiceLocal;
import nl.strohalm.cyclos.services.transactions.TransactionSummaryVO;
import nl.strohalm.cyclos.utils.BigDecimalHelper;
import nl.strohalm.cyclos.utils.DataIteratorHelper;
import nl.strohalm.cyclos.utils.DateHelper;
import nl.strohalm.cyclos.utils.FormatObject;
import nl.strohalm.cyclos.utils.Pair;
import nl.strohalm.cyclos.utils.Period;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.TimePeriod;
import nl.strohalm.cyclos.utils.TimePeriod.Field;
import nl.strohalm.cyclos.utils.cache.Cache;
import nl.strohalm.cyclos.utils.cache.CacheCallback;
import nl.strohalm.cyclos.utils.cache.CacheManager;
import nl.strohalm.cyclos.utils.query.IteratorList;
import nl.strohalm.cyclos.utils.query.PageHelper;
import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import nl.strohalm.cyclos.utils.validation.Validator;
import nl.strohalm.cyclos.utils.validation.Validator.Property;

import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Implementation class for account fee service
 * @author rafael
 * @author luis
 */
public class AccountFeeServiceImpl implements AccountFeeServiceLocal, InitializingService {

    private static int ACCOUNT_FEE_CHARGE_BATCH_SIZE = 20;

    private static final Log LOG = LogFactory.getLog(AccountFeeServiceImpl.class);
    private AccountFeeDAO accountFeeDao;
    private AccountFeeLogDAO accountFeeLogDao;
    private AccountDAO accountDao;
    private FetchServiceLocal fetchService;
    private AccountServiceLocal accountService;
    private MemberAccountFeeLogDAO memberAccountFeeLogDao;
    private CacheManager cacheManager;
    private SettingsServiceLocal settingsService;
    private PaymentServiceLocal paymentService;
    private ApplicationServiceLocal applicationService;

    @Override
    public BigDecimal calculateAmount(final AccountFeeLog feeLog, final Member member) {
        AccountFee fee = feeLog.getAccountFee();

        if (!fee.getGroups().contains(member.getGroup())) {
            // The member is not affected by this fee log
            return null;
        }

        final Period period = feeLog.getPeriod();
        final MemberAccountType accountType = fee.getAccountType();
        final ChargeMode chargeMode = fee.getChargeMode();
        final BigDecimal freeBase = fee.getFreeBase();

        // Calculate the charge amount
        BigDecimal chargedAmount = BigDecimal.ZERO;
        BigDecimal amount = BigDecimal.ZERO;
        Calendar endDate = (period != null) ? period.getEnd() : null;
        final AccountDateDTO balanceParams = new AccountDateDTO(member, accountType, endDate);
        if (chargeMode.isFixed()) {
            boolean charge = true;
            if (freeBase != null) {
                final BigDecimal balance = accountService.getBalance(balanceParams);
                if (balance.compareTo(freeBase) <= 0) {
                    charge = false;
                }
            }
            // Fixed fee amount
            if (charge) {
                amount = feeLog.getAmount();
            }
        } else if (chargeMode.isBalance()) {
            // Percentage over balance
            final boolean positiveBalance = !chargeMode.isNegative();
            BigDecimal balance = accountService.getBalance(balanceParams);
            // Skip if balance is out of range
            boolean charge = true;
            // Apply the free base
            if (freeBase != null) {
                if (positiveBalance) {
                    balance = balance.subtract(freeBase);
                } else {
                    balance = balance.add(freeBase);
                }
            }
            // Check if something will be charged
            if ((positiveBalance && balance.compareTo(BigDecimal.ZERO) <= 0)
                    || (!positiveBalance && balance.compareTo(BigDecimal.ZERO) >= 0)) {
                charge = false;
            }
            if (charge) {
                // Get the charged amount
                chargedAmount = feeLog.getAmountValue().apply(balance.abs());
                amount = settingsService.getLocalSettings().round(chargedAmount);
            }
        } else if (chargeMode.isVolume()) {
            // Percentage over average transactioned volume
            amount = calculateChargeOverTransactionedVolume(feeLog, member);
        }

        // Ensure the amount is valid
        final BigDecimal minPayment = paymentService.getMinimumPayment();
        if (amount.compareTo(minPayment) < 0) {
            amount = BigDecimal.ZERO;
        }
        return amount;
    }

    @Override
    public BigDecimal calculateReservedAmountForVolumeFee(final MemberAccount account) {
        MemberGroup group = (MemberGroup) fetchService.fetch(account.getMember().getGroup());
        AccountFee volumeFee = getVolumeFee(account.getType(), group);
        if (volumeFee == null) {
            return BigDecimal.ZERO;
        }
        // Get the last period which was charged for this account
        AccountFeeLog lastCharged = memberAccountFeeLogDao.getLastChargedLog(account.getMember(), volumeFee);
        Calendar fromDate;
        if (lastCharged == null || lastCharged.getDate().before(volumeFee.getEnabledSince())) {
            // Never charged, or fee re-enabled after the last charge: consider either the account creation date or the fee enabled since -
            // whatever happened later
            fromDate = account.getCreationDate().after(volumeFee.getEnabledSince()) ? account.getCreationDate()
                    : volumeFee.getEnabledSince();
        } else {
            // Already charged - consider the first day on the next period
            fromDate = lastCharged.getPeriod().getEnd();
        }
        // As we calculate by whole days, make sure we're on the next day, so the balance will be ok
        fromDate = DateHelper.truncateNextDay(fromDate);

        // As the volume is an average of days, if there are previous uncharged periods, we must compute each period separately.
        // For example, if the last charge was 2 months ago (a charge failed), we cannot assume that a single charge over 2 months is the
        // same as 2 charges of 1 month, as we charge over the average. So, in the example, if an account has 100 over all time, and we charge 1%,
        // the average for 1 month is 100, so we charge 1. The next month, is the same, and we charge another 1. If the period would be 2 months,
        // The average over those 2 months is still 100, so a single charge of 1 is done, unlike the previous 2 charges.
        // In most normal cases, the loop should be executed only once. Only when an account fee log has failed, it should be executed more than once.
        TimePeriod recurrence = volumeFee.getRecurrence();
        BigDecimal result = BigDecimal.ZERO;
        Calendar now = Calendar.getInstance();
        boolean done = false;
        if (LOG.isDebugEnabled()) {
            LOG.debug("Getting current status for " + account);
        }
        while (!done) {
            Period period = recurrence.currentPeriod(fromDate);
            if (!period.getEnd().after(now)) {
                // Still a past uncharged period
                period.setBegin(fromDate);
                fromDate = DateHelper.truncateNextDay(period.getEnd());
            } else {
                period = Period.between(fromDate, DateHelper.truncate(now));
                done = true;
            }
            BigDecimal chargeForPeriod = calculateVolumeCharge(account, volumeFee, period, result, done);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Charge for period " + FormatObject.formatObject(period.getBegin()) + "\t"
                        + FormatObject.formatObject(period.getEnd()) + "\t" + chargeForPeriod);
            }
            result = result.add(chargeForPeriod);
        }
        return result;
    }

    @Override
    public void chargeManual(AccountFee fee) {
        // Validates the fee
        if (fee == null || fee.isTransient()) {
            throw new UnexpectedEntityException();
        }
        fee = fetchService.fetch(fee, RelationshipHelper.nested(AccountFee.Relationships.ACCOUNT_TYPE,
                AccountType.Relationships.CURRENCY), AccountFee.Relationships.TRANSFER_TYPE);
        if (fee.getRunMode() != RunMode.MANUAL) {
            throw new UnexpectedEntityException();
        }

        // Insert the log with the RUNNING status, so it will be charged
        insertNextExecution(fee);

        applicationService.awakePollingTaskOnTransactionCommit(ChargeAccountFeePollingTask.class);
    }

    @Override
    public int chargeScheduledFees(final Calendar time) {
        final AccountFeeQuery query = new AccountFeeQuery();
        query.setReturnDisabled(false);
        query.setResultType(ResultType.LIST);
        query.setHour((byte) time.get(Calendar.HOUR_OF_DAY));
        query.setType(RunMode.SCHEDULED);
        query.fetch(AccountFee.Relationships.LOGS);
        query.setEnabledBefore(time);

        final List<AccountFee> list = new ArrayList<AccountFee>();
        // Get the daily fees
        query.setRecurrence(TimePeriod.Field.DAYS);
        list.addAll(search(query));
        // Get the weekly fees
        query.setRecurrence(TimePeriod.Field.WEEKS);
        query.setDay((byte) time.get(Calendar.DAY_OF_WEEK));
        list.addAll(search(query));
        // Get the monthly fees
        query.setRecurrence(TimePeriod.Field.MONTHS);
        query.setDay((byte) time.get(Calendar.DAY_OF_MONTH));
        list.addAll(search(query));
        int count = 0;
        for (final AccountFee fee : list) {
            final AccountFeeLog lastExecution = fee.getLastExecution();
            boolean charge;
            if (lastExecution == null) {
                // Was never executed. Charge now
                charge = true;
            } else {
                final TimePeriod recurrence = fee.getRecurrence();
                if (recurrence.getNumber() == 1) {
                    // When recurrence is every day or week or month, charge now
                    charge = true;
                } else {
                    // Check the recurrence
                    final Calendar lastExecutionDate = lastExecution.getDate();
                    if (lastExecutionDate.after(time)) {
                        // Consistency check: don't charge if last execution was after the current time
                        charge = false;
                    }
                    // Find the number of elapsed periods
                    int number = 0;
                    final Calendar cal = DateHelper.truncate(lastExecutionDate);
                    final int calendarField = recurrence.getField().getCalendarValue();
                    final Calendar date = DateHelper.truncate(time);
                    while (cal.before(date)) {
                        number++;
                        cal.add(calendarField, 1);
                    }
                    // Charge each 'x' periods
                    charge = number % recurrence.getNumber() == 0;
                }
            }
            // Charge the fee
            if (charge) {
                insertNextExecution(fee);
                count++;
            }
        }
        return count;
    }

    @Override
    public AccountFeeLog getLastLog(final AccountFee fee) {
        final AccountFeeLogQuery query = new AccountFeeLogQuery();
        query.setAccountFee(fee);
        query.setUniqueResult();
        final List<AccountFeeLog> list = accountFeeLogDao.search(query);
        if (list.isEmpty()) {
            return null;
        } else {
            return list.iterator().next();
        }
    }

    @Override
    public AccountFeeLogDetailsDTO getLogDetails(final Long id) {
        AccountFeeLog log = loadLog(id, AccountFeeLog.Relationships.ACCOUNT_FEE);
        AccountFee fee = log.getAccountFee();
        AccountFeeLogDetailsDTO dto = new AccountFeeLogDetailsDTO();
        dto.setAccountFeeLog(log);
        dto.setSkippedMembers(memberAccountFeeLogDao.countSkippedMembers(log));
        dto.setTransfers(fee.getInvoiceMode() == InvoiceMode.ALWAYS ? new TransactionSummaryVO()
                : memberAccountFeeLogDao.getTransfersSummary(log));
        dto.setInvoices(fee.getInvoiceMode() == InvoiceMode.NEVER ? new TransactionSummaryVO()
                : memberAccountFeeLogDao.getInvoicesSummary(log));
        dto.setAcceptedInvoices(fee.getInvoiceMode() == InvoiceMode.NEVER ? new TransactionSummaryVO()
                : memberAccountFeeLogDao.getAcceptedInvoicesSummary(log));
        dto.setOpenInvoices(dto.getInvoices().subtract(dto.getAcceptedInvoices()));
        return dto;
    }

    @Override
    public void initializeService() {
        insertMissingLogs();
    }

    @Override
    public AccountFee load(final Long id, final Relationship... fetch) {
        return accountFeeDao.load(id, fetch);
    }

    @Override
    public AccountFeeLog loadLog(final Long id, final Relationship... fetch) {
        return accountFeeLogDao.load(id, fetch);
    }

    @Override
    public AccountFeeLog nextLogToCharge() {
        return accountFeeLogDao.nextToCharge();
    }

    @Override
    public List<Member> nextMembersToCharge(final AccountFeeLog feeLog) {
        if (feeLog.isRechargingFailed()) {
            return memberAccountFeeLogDao.nextFailedToCharge(feeLog, ACCOUNT_FEE_CHARGE_BATCH_SIZE);
        } else {
            return memberAccountFeeLogDao.nextToCharge(feeLog, ACCOUNT_FEE_CHARGE_BATCH_SIZE);
        }
    }

    @Override
    public boolean prepareCharge(final AccountFeeLog feeLog) {
        if (feeLog.getTotalMembers() != null) {
            // Already prepared
            return false;
        }
        int totalMembers = memberAccountFeeLogDao.prepareCharge(feeLog);
        feeLog.setTotalMembers(totalMembers);
        accountFeeLogDao.update(feeLog);
        return true;
    }

    @Override
    public void rechargeFailed(final AccountFeeLog accountFeeLog) {
        AccountFeeLog log = fetchService.fetch(accountFeeLog);
        if (log.isRechargingFailed()) {
            // Already recharging failed
            return;
        }
        if (log.getFailedMembers() == 0) {
            // No failures
            return;
        }
        log.setRechargingFailed(true);
        log.setRechargeAttempt(log.getRechargeAttempt() + 1);
        accountFeeLogDao.update(log);

        applicationService.awakePollingTaskOnTransactionCommit(ChargeAccountFeePollingTask.class);
    }

    @Override
    public int remove(final Long... ids) {
        getVolumeFeeByAccountCache().clear();
        return accountFeeDao.delete(ids);
    }

    @Override
    public void removeFromPending(final AccountFeeLog feeLog, final Member member) {
        if (feeLog.isRechargingFailed()) {
            // Remove the MemberAccountFeeLog entirely
            memberAccountFeeLogDao.remove(feeLog, member);
        } else {
            // Just remove the pending charge
            memberAccountFeeLogDao.removePendingCharge(feeLog, member);
        }
    }

    @Override
    public AccountFee save(final AccountFee accountFee) {
        validate(accountFee);

        // Set some attributes to null depending on others
        if (accountFee.getPaymentDirection() == PaymentDirection.TO_MEMBER) {
            // A to member fee never uses invoices
            accountFee.setInvoiceMode(null);
        }
        if (accountFee.getRunMode() == RunMode.MANUAL) {
            // A manual fee does not have recurrence
            accountFee.setRecurrence(null);
            accountFee.setDay(null);
            accountFee.setHour(null);
        }

        // Ensure the cache for volume fees is cleared
        getVolumeFeeByAccountCache().clear();

        // Persist the account fee
        if (accountFee.isTransient()) {
            if (accountFee.isEnabled() && accountFee.getEnabledSince() == null) {
                accountFee.setEnabledSince(Calendar.getInstance());
            }
            return accountFeeDao.insert(accountFee);
        } else {
            final AccountFee current = load(accountFee.getId());
            // Correctly handle the enabled since
            if (accountFee.isEnabled() && current.getEnabledSince() == null) {
                // When was not previously enabled, initialize the enabled since
                if (accountFee.getEnabledSince() == null) {
                    accountFee.setEnabledSince(Calendar.getInstance());
                }
            } else if (!accountFee.isEnabled() && current.isEnabled()) {
                // When is disabling, set the date to null
                accountFee.setEnabledSince(null);
            } else if (accountFee.getEnabledSince() == null) {
                // Just updating other fields - keep the enabled since
                accountFee.setEnabledSince(current.getEnabledSince());
            }
            return accountFeeDao.update(accountFee);
        }
    }

    @Override
    public AccountFeeLog save(final AccountFeeLog accountFeeLog) {
        if (accountFeeLog.isTransient()) {
            return accountFeeLogDao.insert(accountFeeLog);
        } else {
            return accountFeeLogDao.update(accountFeeLog);
        }
    }

    @Override
    public List<AccountFee> search(final AccountFeeQuery query) {
        return accountFeeDao.search(query);
    }

    @Override
    public List<AccountFeeLog> searchLogs(final AccountFeeLogQuery query) {
        return accountFeeLogDao.search(query);
    }

    @Override
    public List<MemberAccountFeeLog> searchMembers(final MemberAccountFeeLogQuery query) {
        LocalSettings localSettings = settingsService.getLocalSettings();
        return memberAccountFeeLogDao.search(query, localSettings.getMemberResultDisplay());
    }

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

    public void setAccountFeeDao(final AccountFeeDAO dao) {
        accountFeeDao = dao;
    }

    public void setAccountFeeLogDao(final AccountFeeLogDAO accountFeeLogDao) {
        this.accountFeeLogDao = accountFeeLogDao;
    }

    public void setAccountServiceLocal(final AccountServiceLocal accountService) {
        this.accountService = accountService;
    }

    public void setApplicationServiceLocal(final ApplicationServiceLocal applicationService) {
        this.applicationService = applicationService;
    }

    public void setCacheManager(final CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    @Override
    public MemberAccountFeeLog setChargingError(final AccountFeeLog feeLog, final Member member,
            final BigDecimal amount) {
        removeFromPending(feeLog, member);

        final MemberAccountFeeLog mafl = new MemberAccountFeeLog();
        mafl.setDate(Calendar.getInstance());
        mafl.setAccountFeeLog(feeLog);
        mafl.setMember(member);
        mafl.setAmount(amount);
        mafl.setSuccess(false);
        mafl.setRechargeAttempt(feeLog.getRechargeAttempt());
        return memberAccountFeeLogDao.insert(mafl);
    }

    @Override
    public MemberAccountFeeLog setChargingSuccess(final AccountFeeLog feeLog, final Member member,
            final BigDecimal amount, final Transfer transfer, final Invoice invoice) {
        removeFromPending(feeLog, member);

        MemberAccountFeeLog mafl = null;

        if (feeLog.isRechargingFailed()) {
            // Load the failed log
            mafl = memberAccountFeeLogDao.load(feeLog, member);
            if (mafl != null && mafl.isSuccess()) {
                // Nothing to do with this member, as it was not a charge failure
                return null;
            }
        }

        if (mafl == null) {
            mafl = new MemberAccountFeeLog();
            mafl.setAccountFeeLog(feeLog);
            mafl.setMember(member);
        }
        mafl.setDate(Calendar.getInstance());
        mafl.setAmount(amount);
        mafl.setSuccess(true);
        mafl.setTransfer(transfer);
        mafl.setInvoice(invoice);

        if (mafl.isTransient()) {
            return memberAccountFeeLogDao.insert(mafl);
        } else {
            return memberAccountFeeLogDao.update(mafl);
        }
    }

    public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
        this.fetchService = fetchService;
    }

    public void setMemberAccountFeeLogDao(final MemberAccountFeeLogDAO memberAccountFeeLogDao) {
        this.memberAccountFeeLogDao = memberAccountFeeLogDao;
    }

    public void setPaymentServiceLocal(final PaymentServiceLocal paymentService) {
        this.paymentService = paymentService;
    }

    public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
        this.settingsService = settingsService;
    }

    @Override
    public void validate(final AccountFee accountFee) {
        getValidator(accountFee).validate(accountFee);
    }

    private BigDecimal calculateChargeOverTransactionedVolume(final AccountFeeLog feeLog, final Member member) {
        AccountFee fee = feeLog.getAccountFee();
        if (!fee.isEnabled()) {
            return BigDecimal.ZERO;
        }
        MemberAccount account = (MemberAccount) accountService
                .getAccount(new AccountDTO(member, fee.getAccountType()));

        // We want to limit for diffs within the fee log period
        Period logPeriod = feeLog.getPeriod();
        Calendar beginDate = logPeriod.getBegin();
        if (fee.getEnabledSince().after(beginDate)) {
            // However, if the fee was enabled in the middle of the period, consider this date instead
            beginDate = fee.getEnabledSince();
        }
        if (account.getCreationDate().after(beginDate)) {
            // If the account was created after, use it's creation date
            beginDate = account.getCreationDate();
        }
        // As we calculate by whole days, make sure we're on the next day, so the balance will be ok
        beginDate = DateHelper.truncateNextDay(beginDate);

        Period period = Period.between(beginDate, logPeriod.getEnd());
        if (period.getBegin().after(period.getEnd())) {
            // In case of single days, the begin is the next day, and the end is the last second of the current day
            period.setEnd(period.getBegin());
        }
        return calculateVolumeCharge(account, fee, period, BigDecimal.ZERO, false);
    }

    private BigDecimal calculateVolumeCharge(final MemberAccount account, final AccountFee volumeFee,
            final Period period, final BigDecimal additionalReserved, final boolean currentPeriod) {
        Calendar fromDate = period.getBegin();

        // Get the account status right after the last charged period
        AccountStatus status = accountService.getStatus(account, fromDate);

        // When there is some additional amount to consider as reserved, add it to the status
        status.setReservedAmount(status.getReservedAmount().add(additionalReserved));

        // Calculate the total days. We want the entire charged period. For example: if the fee was enabled in the middle of a month, it would be the
        // entire month. Likewise, if no end limit was given, the current period will be used (ie, and the last day in the current month)
        TimePeriod recurrence = volumeFee.getRecurrence();
        Period totalPeriod = recurrence.currentPeriod(period.getBegin());
        int totalDays = totalPeriod.getDays() + 1;

        // Calculate each difference, with the corresponding reserved amount
        Calendar lastDay = fromDate;
        Calendar lastChargedDay = fromDate;
        BigDecimal result = BigDecimal.ZERO;
        IteratorList<AccountDailyDifference> diffs = accountDao.iterateDailyDifferences(account, period);
        if (LOG.isDebugEnabled()) {
            LOG.debug("********************************");
            LOG.debug(FormatObject.formatObject(period.getBegin()) + "\t" + status.getBalance() + "\t"
                    + status.getAvailableBalance());
        }
        try {
            if (diffs.hasNext()) {
                // There are differences - the lastChargedAvailable balance will be obtained within the loop
                for (AccountDailyDifference diff : diffs) {
                    Calendar day = diff.getDay();
                    int days = DateHelper.daysBetween(lastDay, day);
                    // Get the available balance at that day
                    BigDecimal available = status.getAvailableBalanceWithoutCreditLimit();
                    if (volumeFee.getChargeMode().isNegative()) {
                        // If the charge is over negative amounts, consider the negated amount
                        available = available.negate();
                    }
                    // Take the free base into account
                    if (volumeFee.getFreeBase() != null) {
                        available = available.subtract(volumeFee.getFreeBase());
                    }
                    // If the available balance was significant, calculate the charge
                    if (available.compareTo(BigDecimal.ZERO) > 0) {
                        BigDecimal volume = new BigDecimal(available.doubleValue() * days / totalDays);
                        if (LOG.isDebugEnabled()) {
                            LOG.debug(FormatObject.formatObject(day) + "\t" + diff.getBalance() + "\t"
                                    + status.getAvailableBalanceWithoutCreditLimit().add(diff.getBalance()) + "\t"
                                    + days + "\t" + totalDays + "\t" + volume);
                        }
                        BigDecimal toCharge = volume.multiply(volumeFee.getAmount())
                                .divide(BigDecimalHelper.ONE_HUNDRED);
                        // status.setReservedAmount(status.getReservedAmount().add(toCharge));
                        result = result.add(toCharge);
                        lastChargedDay = day;
                    }
                    lastDay = day;
                    status.setBalance(status.getBalance().add(diff.getBalance()));
                    status.setReservedAmount(status.getReservedAmount().add(diff.getReserved()));
                }
            }
        } finally {
            DataIteratorHelper.close(diffs);
        }

        Calendar toDate = period.getEnd();
        boolean lastPaymentInPeriodEnd = !toDate.before(lastChargedDay);
        LocalSettings settings = settingsService.getLocalSettings();

        // Only if the last payment was not today we have to take into account the results so far
        if (DateHelper.daysBetween(lastChargedDay, Calendar.getInstance()) != 0) {
            BigDecimal resultSoFar = settings.round(result);
            status.setReservedAmount(status.getReservedAmount().add(resultSoFar));
        }

        // Calculate the avaliable balance after the last diff, which will remain the same until the period end
        BigDecimal finalAvailableBalance = status.getAvailableBalanceWithoutCreditLimit();
        if (volumeFee.getChargeMode().isNegative()) {
            finalAvailableBalance = finalAvailableBalance.negate();
        }
        if (volumeFee.getFreeBase() != null) {
            finalAvailableBalance = finalAvailableBalance.subtract(volumeFee.getFreeBase());
        }

        // Consider the last time slice, between the last diff and the period end, if any
        if (lastPaymentInPeriodEnd && finalAvailableBalance.compareTo(BigDecimal.ZERO) > 0) {
            int days = DateHelper.daysBetween(lastChargedDay, toDate) + (currentPeriod ? 0 : 1);
            // Here, the lastChargedAvailableBalance is already subtracted from the free base (if any)
            BigDecimal volume = new BigDecimal(finalAvailableBalance.doubleValue() * days / totalDays);
            BigDecimal toCharge = volume.multiply(volumeFee.getAmount()).divide(BigDecimalHelper.ONE_HUNDRED);
            result = result.add(toCharge);
            if (LOG.isDebugEnabled()) {
                status.setReservedAmount(settings.round(status.getReservedAmount().add(toCharge)));
                LOG.debug(FormatObject.formatObject(lastChargedDay) + "\t0\t"
                        + status.getAvailableBalanceWithoutCreditLimit() + "\t" + days + "\t" + totalDays + "\t"
                        + volume);
            }
        }

        return settings.round(result);
    }

    /**
     * Returns the missing periods for an account fee
     */
    private List<Period> getMissingPeriods(final AccountFee fee) {
        final TimePeriod recurrence = fee.getRecurrence();
        Calendar since;
        final Calendar now = DateUtils.truncate(Calendar.getInstance(), Calendar.HOUR_OF_DAY);

        // Determine since when the fee should have run
        final AccountFeeLog lastLog = getLastLog(fee);
        if (lastLog == null || lastLog.getDate().before(fee.getEnabledSince())) {
            // May be 2 cases: Either the fee never ran or was re-enabled after the last run
            since = fee.getEnabledSince();
        } else {
            // The fee ran and is enabled, just has missing logs
            since = lastLog.getDate();
        }

        // Resolve the periods
        final List<Period> periods = new ArrayList<Period>();
        Calendar date = DateHelper.truncate(since);
        Period period = recurrence.previousPeriod(date);
        while (true) {
            date = (Calendar) period.getEnd().clone();
            date.add(Calendar.SECOND, 1);
            period = recurrence.periodStartingAt(date);
            if (period.getEnd().before(now)) {
                periods.add(period);
            } else {
                break;
            }
        }

        // Check if the last one should be really there
        if (!periods.isEmpty()) {
            // Do not use the last period if the listener has not run yet
            final byte thisDay = (byte) now.get(Calendar.DAY_OF_MONTH);
            final byte thisHour = (byte) now.get(Calendar.HOUR_OF_DAY);
            boolean removeLast = false;

            final Byte feeDay = fee.getDay();
            if (feeDay != null && thisDay < feeDay) {
                removeLast = true;
            } else if (feeDay == null || thisDay == feeDay) {
                removeLast = thisHour < fee.getHour();
            }
            if (removeLast) {
                periods.remove(periods.size() - 1);
            }
        }

        // Check if any of those logs are present
        final Iterator<Period> it = periods.iterator();
        while (it.hasNext()) {
            final Period current = it.next();
            final AccountFeeLogQuery logQuery = new AccountFeeLogQuery();
            logQuery.setPageForCount();
            logQuery.setAccountFee(fee);
            logQuery.setPeriodStartAt(current.getBegin());
            final int count = PageHelper.getTotalCount(accountFeeLogDao.search(logQuery));
            if (count > 0) {
                it.remove();
            }
        }
        return periods;
    }

    private Validator getValidator(final AccountFee fee) {
        final Validator validator = new Validator("accountFee");
        validator.property("accountType").required();
        validator.property("transferType").required();
        validator.property("name").required().maxLength(100);
        validator.property("description").maxLength(1000);
        validator.property("amount").required().positiveNonZero();
        validator.property("chargeMode").required();
        validator.property("paymentDirection").required();
        Property runMode = validator.property("runMode").required();
        if (fee.getChargeMode() != null && fee.getChargeMode().isVolume()) {
            runMode.anyOf(RunMode.SCHEDULED);
        }
        if (fee.getRunMode() == RunMode.SCHEDULED) {
            validator.property("recurrence.number").key("accountFee.recurrence").required().positiveNonZero()
                    .lessThan(28);
            validator.property("recurrence.field").key("accountFee.recurrence").required()
                    .anyOf(TimePeriod.Field.DAYS, TimePeriod.Field.WEEKS, TimePeriod.Field.MONTHS);
            Property day = validator.property("day");
            Field field = fee.getRecurrence() == null ? null : fee.getRecurrence().getField();
            if (field != null && field != Field.DAYS) {
                day.required();
                if (field == Field.WEEKS) {
                    day.between(1, 7);
                } else {
                    day.between(1, 28);
                }
            }
            validator.property("hour").required().between(0, 23);
        }
        if (fee.isMemberToSystem()) {
            validator.property("invoiceMode").required();
        }
        if (fee.isTransient()) {
            validator.property("enabledSince").key("accountFee.firstPeriodAfter").futureOrToday();
        }
        return validator;
    }

    private AccountFee getVolumeFee(final AccountType accountType, final MemberGroup group) {
        Pair<Long, Long> key = new Pair<Long, Long>(accountType.getId(), group.getId());
        return getVolumeFeeByAccountCache().get(key, new CacheCallback() {
            @Override
            public Object retrieve() {
                final AccountFeeQuery query = new AccountFeeQuery();
                query.setAccountType(accountType);
                query.setGroups(Collections.singleton(group));
                query.setType(RunMode.SCHEDULED);
                List<AccountFee> fees = search(query);
                for (Iterator<AccountFee> iterator = fees.iterator(); iterator.hasNext();) {
                    if (!iterator.next().getChargeMode().isVolume()) {
                        iterator.remove();
                    }
                }
                if (fees.size() > 1) {
                    throw new ValidationException("accountFee.error.multipleVolumeFees");
                }
                return fees.isEmpty() ? null : fees.iterator().next();
            }
        });
    }

    private Cache getVolumeFeeByAccountCache() {
        return cacheManager.getCache("cyclos.VolumeFeeByAccount");
    }

    private void insertMissingLogs() {
        final AccountFeeQuery query = new AccountFeeQuery();
        query.setReturnDisabled(false);
        query.setType(RunMode.SCHEDULED);
        Calendar thisHour = DateUtils.truncate(Calendar.getInstance(), Calendar.HOUR_OF_DAY);
        final List<AccountFee> accountFees = accountFeeDao.search(query);
        for (final AccountFee fee : accountFees) {
            final Field recurrenceField = fee.getRecurrence().getField();
            final List<Period> missingPeriods = getMissingPeriods(fee);
            if (!missingPeriods.isEmpty()) {
                for (final Period period : missingPeriods) {
                    final Calendar shouldHaveChargedAt = DateHelper.truncate(period.getEnd());
                    shouldHaveChargedAt.add(Calendar.DAY_OF_MONTH, 1);
                    switch (recurrenceField) {
                    case WEEKS:
                        // Go to the day it should have been charged
                        int max = 7;
                        while (max > 0 && shouldHaveChargedAt.get(Calendar.DAY_OF_WEEK) < fee.getDay()) {
                            shouldHaveChargedAt.add(Calendar.DAY_OF_MONTH, 1);
                            max--;
                        }
                        break;
                    case MONTHS:
                        shouldHaveChargedAt.set(Calendar.DAY_OF_MONTH, fee.getDay());
                        break;
                    }
                    shouldHaveChargedAt.set(Calendar.HOUR_OF_DAY, fee.getHour());
                    // Only insert the missing log if it should have run before or at this hour
                    if (!shouldHaveChargedAt.after(thisHour)) {
                        final AccountFeeLog log = new AccountFeeLog();
                        log.setAccountFee(fee);
                        log.setDate(shouldHaveChargedAt);
                        log.setPeriod(period);
                        log.setAmount(fee.getAmount());
                        log.setFreeBase(fee.getFreeBase());
                        accountFeeLogDao.insert(log);
                    }
                }
            }
        }
    }

    private AccountFeeLog insertNextExecution(AccountFee fee) {
        fee = fetchService.fetch(fee);
        if (!fee.isEnabled()) {
            return null;
        }

        // Resolve the period
        Period period = null;
        Calendar executionDate = null;
        if (fee.getRunMode() == RunMode.MANUAL) {
            // Manual fee
            executionDate = Calendar.getInstance();
        } else {
            // Scheduled fee
            executionDate = fee.getNextExecutionDate();
            // Do not insert future account fees.
            if (executionDate.after(Calendar.getInstance())) {
                return null;
            }
            period = fee.getRecurrence().previousPeriod(executionDate);
        }

        // Create the log
        AccountFeeLog nextExecution = new AccountFeeLog();
        nextExecution.setAccountFee(fee);
        nextExecution.setDate(executionDate);
        nextExecution.setPeriod(period);
        nextExecution.setAmount(fee.getAmount());
        nextExecution.setFreeBase(fee.getFreeBase());
        return accountFeeLogDao.insert(nextExecution);
    }

}