nl.strohalm.cyclos.services.transactions.PaymentServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.services.transactions.PaymentServiceImpl.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.transactions;

import java.awt.Color;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import nl.strohalm.cyclos.access.AdminMemberPermission;
import nl.strohalm.cyclos.access.AdminSystemPermission;
import nl.strohalm.cyclos.access.BrokerPermission;
import nl.strohalm.cyclos.access.MemberPermission;
import nl.strohalm.cyclos.access.OperatorPermission;
import nl.strohalm.cyclos.dao.accounts.transactions.ScheduledPaymentDAO;
import nl.strohalm.cyclos.dao.accounts.transactions.TraceNumberDAO;
import nl.strohalm.cyclos.dao.accounts.transactions.TransferDAO;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.access.Channel;
import nl.strohalm.cyclos.entities.accounts.Account;
import nl.strohalm.cyclos.entities.accounts.AccountOwner;
import nl.strohalm.cyclos.entities.accounts.AccountStatus;
import nl.strohalm.cyclos.entities.accounts.AccountType;
import nl.strohalm.cyclos.entities.accounts.Currency;
import nl.strohalm.cyclos.entities.accounts.LockedAccountsOnPayments;
import nl.strohalm.cyclos.entities.accounts.MemberAccount;
import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner;
import nl.strohalm.cyclos.entities.accounts.SystemAccountType;
import nl.strohalm.cyclos.entities.accounts.external.ExternalTransfer;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.BrokerCommission;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.SimpleTransactionFee;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.TransactionFee;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.TransactionFee.ChargeType;
import nl.strohalm.cyclos.entities.accounts.fees.transaction.TransactionFeeQuery;
import nl.strohalm.cyclos.entities.accounts.transactions.AuthorizationLevel;
import nl.strohalm.cyclos.entities.accounts.transactions.Invoice;
import nl.strohalm.cyclos.entities.accounts.transactions.Payment;
import nl.strohalm.cyclos.entities.accounts.transactions.PaymentRequestTicket;
import nl.strohalm.cyclos.entities.accounts.transactions.ScheduledPayment;
import nl.strohalm.cyclos.entities.accounts.transactions.Ticket;
import nl.strohalm.cyclos.entities.accounts.transactions.TraceNumber;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferListener;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferQuery;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferType;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferType.Relationships;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferTypeQuery;
import nl.strohalm.cyclos.entities.alerts.MemberAlert;
import nl.strohalm.cyclos.entities.customization.fields.CustomField;
import nl.strohalm.cyclos.entities.customization.fields.PaymentCustomField;
import nl.strohalm.cyclos.entities.customization.fields.PaymentCustomFieldValue;
import nl.strohalm.cyclos.entities.exceptions.DaoException;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.groups.AdminGroup;
import nl.strohalm.cyclos.entities.groups.BrokerGroup;
import nl.strohalm.cyclos.entities.groups.Group;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.groups.OperatorGroup;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.members.Operator;
import nl.strohalm.cyclos.entities.members.brokerings.BrokerCommissionContract;
import nl.strohalm.cyclos.entities.members.brokerings.BrokerCommissionContractQuery;
import nl.strohalm.cyclos.entities.reports.StatisticalNumber;
import nl.strohalm.cyclos.entities.services.ServiceClient;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.entities.settings.LocalSettings.TransactionNumber;
import nl.strohalm.cyclos.exceptions.ApplicationException;
import nl.strohalm.cyclos.exceptions.PermissionDeniedException;
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.accounts.rates.ConversionSimulationDTO;
import nl.strohalm.cyclos.services.accounts.rates.RateServiceLocal;
import nl.strohalm.cyclos.services.accounts.rates.RatesPreviewDTO;
import nl.strohalm.cyclos.services.accounts.rates.RatesResultDTO;
import nl.strohalm.cyclos.services.accounts.rates.RatesToSave;
import nl.strohalm.cyclos.services.alerts.AlertServiceLocal;
import nl.strohalm.cyclos.services.application.ApplicationServiceLocal;
import nl.strohalm.cyclos.services.customization.PaymentCustomFieldServiceLocal;
import nl.strohalm.cyclos.services.elements.CommissionServiceLocal;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.services.stats.StatisticalResultDTO;
import nl.strohalm.cyclos.services.transactions.exceptions.AuthorizedPaymentInPastException;
import nl.strohalm.cyclos.services.transactions.exceptions.MaxAmountPerDayExceededException;
import nl.strohalm.cyclos.services.transactions.exceptions.NotEnoughCreditsException;
import nl.strohalm.cyclos.services.transactions.exceptions.TransferMinimumPaymentException;
import nl.strohalm.cyclos.services.transactions.exceptions.UpperCreditLimitReachedException;
import nl.strohalm.cyclos.services.transfertypes.BuildTransferWithFeesDTO;
import nl.strohalm.cyclos.services.transfertypes.TransactionFeePreviewDTO;
import nl.strohalm.cyclos.services.transfertypes.TransactionFeePreviewForRatesDTO;
import nl.strohalm.cyclos.services.transfertypes.TransactionFeeServiceLocal;
import nl.strohalm.cyclos.services.transfertypes.TransferTypeServiceLocal;
import nl.strohalm.cyclos.utils.BaseTransactional;
import nl.strohalm.cyclos.utils.BigDecimalHelper;
import nl.strohalm.cyclos.utils.CacheCleaner;
import nl.strohalm.cyclos.utils.CustomObjectHandler;
import nl.strohalm.cyclos.utils.DataIteratorHelper;
import nl.strohalm.cyclos.utils.DateHelper;
import nl.strohalm.cyclos.utils.MessageProcessingHelper;
import nl.strohalm.cyclos.utils.MessageResolver;
import nl.strohalm.cyclos.utils.Period;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.TimePeriod;
import nl.strohalm.cyclos.utils.TransactionHelper;
import nl.strohalm.cyclos.utils.Transactional;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.conversion.CalendarConverter;
import nl.strohalm.cyclos.utils.conversion.CoercionHelper;
import nl.strohalm.cyclos.utils.lock.LockHandler;
import nl.strohalm.cyclos.utils.lock.LockHandlerFactory;
import nl.strohalm.cyclos.utils.logging.LoggingHandler;
import nl.strohalm.cyclos.utils.notifications.AdminNotificationHandler;
import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler;
import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType;
import nl.strohalm.cyclos.utils.statistics.GraphHelper;
import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData;
import nl.strohalm.cyclos.utils.transaction.TransactionCommitListener;
import nl.strohalm.cyclos.utils.validation.CompareToValidation;
import nl.strohalm.cyclos.utils.validation.DelegatingValidator;
import nl.strohalm.cyclos.utils.validation.GeneralValidation;
import nl.strohalm.cyclos.utils.validation.InvalidError;
import nl.strohalm.cyclos.utils.validation.PropertyValidation;
import nl.strohalm.cyclos.utils.validation.RequiredError;
import nl.strohalm.cyclos.utils.validation.UniqueError;
import nl.strohalm.cyclos.utils.validation.ValidationError;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import nl.strohalm.cyclos.utils.validation.Validator;
import nl.strohalm.cyclos.utils.validation.Validator.Property;
import nl.strohalm.cyclos.webservices.accounts.AccountHistoryResultPage;
import nl.strohalm.cyclos.webservices.model.AccountHistoryTransferVO;
import nl.strohalm.cyclos.webservices.model.AccountStatusVO;
import nl.strohalm.cyclos.webservices.payments.AccountHistoryParams;
import nl.strohalm.cyclos.webservices.utils.AccountHelper;
import nl.strohalm.cyclos.webservices.utils.PaymentHelper;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jfree.chart.plot.CategoryMarker;
import org.jfree.chart.plot.Marker;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;

/**
 * Implementation for payment service
 * @author luis
 * @author rinke (rates stuff)
 */
public class PaymentServiceImpl implements PaymentServiceLocal {

    /**
     * A key to monitor which fees have been charged, in order to detect loops
     * @author luis
     */
    private static class ChargedFee {
        private final TransactionFee fee;
        private final Account from;
        private final Account to;

        private ChargedFee(final TransactionFee fee, final Account from, final Account to) {
            this.fee = fee;
            this.from = from;
            this.to = to;
        }

        @Override
        public boolean equals(final Object obj) {
            if (!(obj instanceof ChargedFee)) {
                return false;
            }
            final ChargedFee f = (ChargedFee) obj;
            return new EqualsBuilder().append(fee, f.fee).append(from, f.from).append(to, f.to).isEquals();
        }

        @Override
        public int hashCode() {
            return new HashCodeBuilder().append(fee).append(from).append(to).toHashCode();
        }
    }

    /**
     * validator always returning a validationError. To be called if the final amount of a payment (after applying all fees) is negative
     * @author rinke
     */
    private final class FinalAmountValidator implements GeneralValidation {
        private static final long serialVersionUID = -2789145696000017181L;

        @Override
        public ValidationError validate(final Object object) {
            return new ValidationError("payment.error.negativeFinalAmount");
        }
    }

    /**
     * validator which always returns a validationError. To be called if a past date on a transfer is combined with rates.
     * @author Rinke
     * 
     */
    private static final class NoPastDateWithRatesValidator implements GeneralValidation {

        private static final long serialVersionUID = -6914314732478889087L;

        @Override
        public ValidationError validate(final Object object) {
            return new ValidationError("payment.error.pastDateWithRates");
        }
    }

    private final class PendingContractValidator implements GeneralValidation {

        private static final long serialVersionUID = 5608258953479316287L;

        @Override
        @SuppressWarnings("unchecked")
        public ValidationError validate(final Object object) {
            // Validate the scheduled payments
            final DoPaymentDTO payment = (DoPaymentDTO) object;

            Member fromMember = (Member) (payment.getFrom() instanceof Member ? payment.getFrom()
                    : payment.getFrom() == null ? LoggedUser.member() : null);
            if (fromMember != null) {
                fromMember = fetchService.fetch(fromMember, Element.Relationships.GROUP);

                // Validate if there is a fee (broker commission) with a pending contract
                if (payment.getTo() != null && payment.getTo() instanceof Member
                        && payment.getTransferType() != null) {
                    final TransferType transferType = fetchService.fetch(payment.getTransferType(),
                            TransferType.Relationships.TRANSACTION_FEES);
                    final Collection<TransactionFee> transactionFees = (Collection<TransactionFee>) fetchService
                            .fetch(transferType.getTransactionFees(),
                                    TransactionFee.Relationships.GENERATED_TRANSFER_TYPE);
                    for (final TransactionFee transactionFee : transactionFees) {
                        if (transactionFee instanceof BrokerCommission && transactionFee.isFromMember()) {
                            final BrokerCommission brokerCommission = (BrokerCommission) transactionFee;
                            final BrokerCommissionContractQuery contractsQuery = new BrokerCommissionContractQuery();
                            contractsQuery.setBrokerCommission(brokerCommission);
                            contractsQuery.setStatus(BrokerCommissionContract.Status.PENDING);
                            switch (brokerCommission.getWhichBroker()) {
                            case SOURCE:
                                contractsQuery.setMember(fromMember);
                                break;
                            case DESTINATION:
                                contractsQuery.setMember((Member) payment.getTo());
                                break;
                            }
                            final List<BrokerCommissionContract> commissionContracts = commissionService
                                    .searchBrokerCommissionContracts(contractsQuery);
                            if (CollectionUtils.isNotEmpty(commissionContracts)) {
                                return new ValidationError("payment.error.pendingCommissionContract",
                                        brokerCommission.getName());
                            }
                        }
                    }
                }
            }
            return null;
        }
    }

    private final class SchedulingValidator implements GeneralValidation {

        private static final long serialVersionUID = 4085922259108191939L;

        @Override
        @SuppressWarnings("unchecked")
        public ValidationError validate(final Object object) {
            // Validate the scheduled payments
            final DoPaymentDTO payment = (DoPaymentDTO) object;
            final List<ScheduledPaymentDTO> payments = payment.getPayments();
            if (CollectionUtils.isEmpty(payments)) {
                return null;
            }

            final TransferType transferType = fetchService.fetch(payment.getTransferType(),
                    TransferType.Relationships.TRANSACTION_FEES);

            // It is assumed that the validation where this Validator is used, checks the requirement of the transferType.
            // So it's safe to return, cause the validation will fail.
            if (transferType == null) {
                return null;
            }

            // Validate the from member
            Member fromMember = null;
            if (payment.getFrom() instanceof Member) {
                fromMember = fetchService.fetch((Member) payment.getFrom(), Element.Relationships.GROUP);
            } else if (LoggedUser.hasUser() && LoggedUser.isMember()) {
                fromMember = LoggedUser.element();
            }
            Calendar maxPaymentDate = null;
            if (fromMember != null) {
                final MemberGroup group = fromMember.getMemberGroup();

                // Validate the max payments
                final int maxSchedulingPayments = transferType.isAllowsScheduledPayments()
                        ? group.getMemberSettings().getMaxSchedulingPayments()
                        : 0;
                if (payments.size() > maxSchedulingPayments) {
                    return new ValidationError("errors.greaterEquals",
                            messageResolver.message("transfer.paymentCount"), maxSchedulingPayments);
                }

                // Get the maximum payment date
                final TimePeriod maxSchedulingPeriod = group.getMemberSettings().getMaxSchedulingPeriod();
                if (maxSchedulingPeriod != null) {
                    maxPaymentDate = maxSchedulingPeriod.add(DateHelper.truncate(Calendar.getInstance()));
                }

                // Validate if there is a fee with a pending contract
                if (payment.getTo() != null && payment.getTo() instanceof Member) {
                    final Collection<TransactionFee> transactionFees = (Collection<TransactionFee>) fetchService
                            .fetch(transferType.getTransactionFees(),
                                    TransactionFee.Relationships.GENERATED_TRANSFER_TYPE);
                    for (final TransactionFee transactionFee : transactionFees) {
                        if (transactionFee instanceof BrokerCommission && transactionFee.isFromMember()) {
                            final BrokerCommission brokerCommission = (BrokerCommission) transactionFee;
                            final BrokerCommissionContractQuery contractsQuery = new BrokerCommissionContractQuery();
                            contractsQuery.setBrokerCommission(brokerCommission);
                            contractsQuery.setStatus(BrokerCommissionContract.Status.PENDING);
                            switch (brokerCommission.getWhichBroker()) {
                            case SOURCE:
                                contractsQuery.setMember(fromMember);
                                break;
                            case DESTINATION:
                                contractsQuery.setMember((Member) payment.getTo());
                                break;
                            }
                            final List<BrokerCommissionContract> commissionContracts = commissionService
                                    .searchBrokerCommissionContracts(contractsQuery);
                            if (CollectionUtils.isNotEmpty(commissionContracts)) {
                                return new ValidationError("payment.error.pendingCommissionContract",
                                        brokerCommission.getName());
                            }
                        }
                    }
                }

            }

            // Validate the total payment amount and dates
            final BigDecimal paymentAmount = payment.getAmount();
            final BigDecimal minimumPayment = getMinimumPayment();
            BigDecimal totalAmount = BigDecimal.ZERO;
            Calendar lastDate = DateHelper.truncatePreviosDay(Calendar.getInstance());
            for (final ScheduledPaymentDTO dto : payments) {
                final Calendar date = dto.getDate();
                // Validate the max payment date
                if (maxPaymentDate != null && date.after(maxPaymentDate)) {
                    final LocalSettings localSettings = settingsService.getLocalSettings();
                    final CalendarConverter dateConverter = localSettings.getRawDateConverter();
                    return new ValidationError("payment.invalid.schedulingDate",
                            dateConverter.toString(maxPaymentDate));
                }

                final BigDecimal amount = dto.getAmount();

                if (amount == null || amount.compareTo(minimumPayment) < 0) {
                    return new RequiredError(messageResolver.message("transfer.amount"));
                }

                BigDecimal minAmount = transferType.getMinAmount();
                if (minAmount != null) {
                    if (amount.compareTo(minAmount) < 0) {
                        return new ValidationError("errors.greaterEquals", amount, minAmount);
                    }
                }

                if (date == null) {
                    return new RequiredError(messageResolver.message("transfer.date"));
                } else if (date.before(lastDate) || DateUtils.isSameDay(date, lastDate)) {
                    return new ValidationError("payment.invalid.paymentDates");
                }
                totalAmount = totalAmount.add(amount);
                lastDate = date;
            }
            // Validate the total payment amount
            if (paymentAmount != null && totalAmount.compareTo(paymentAmount) != 0) {
                return new ValidationError("payment.invalid.paymentAmount");
            }
            return null;
        }
    }

    private class TicketValidation implements PropertyValidation {

        private static final long serialVersionUID = 1L;

        @Override
        public ValidationError validate(final Object object, final Object property, final Object value) {
            if (value != null) {
                DoPaymentDTO dto = (DoPaymentDTO) object;
                Ticket ticket = (Ticket) value;
                if (ticket != null && dto.getChannel() != Channel.WEBSHOP) {
                    return new InvalidError();
                } else {
                    try {
                        ticket = fetchService.fetch(ticket);
                        if (ticket != null && ticket.getStatus() != Ticket.Status.PENDING) {
                            throw new EntityNotFoundException(Ticket.class);
                        }
                    } catch (EntityNotFoundException e) {
                        return new InvalidError();
                    }
                }
            }
            return null;
        }

    }

    private class TraceNumberValidation implements PropertyValidation {

        private static final long serialVersionUID = 2424106851078796317L;

        @Override
        public ValidationError validate(final Object object, final Object property, final Object value) {
            final TransferDTO dto = (TransferDTO) object;
            final Long clientId = dto.getClientId();
            final String traceNumber = dto.getTraceNumber();
            if (clientId == null || StringUtils.isEmpty(traceNumber)) {
                return null;
            }
            try {
                transferDao.loadTransferByTraceNumber(traceNumber, clientId);
                traceNumberDao.load(clientId, traceNumber);
                // Invalid, as if it reaches here, there is at least one other transfer with the given trace number
                return new UniqueError(traceNumber);
            } catch (final EntityNotFoundException e) {
                // Is valid, as there are no other transfer using that trace number for that client id
                return null;
            }
        }
    }

    private static final float PRECISION_DELTA = 0.0001F;
    private static final Relationship[] CONCILIATION_FETCH = { Transfer.Relationships.EXTERNAL_TRANSFER,
            RelationshipHelper.nested(Payment.Relationships.FROM, MemberAccount.Relationships.MEMBER) };

    private AccountServiceLocal accountService;
    private CommissionServiceLocal commissionService;
    private SettingsServiceLocal settingsService;
    private TransferAuthorizationServiceLocal transferAuthorizationService;
    private TicketServiceLocal ticketService;
    private TransactionFeeServiceLocal transactionFeeService;
    private TransferDAO transferDao;
    private TraceNumberDAO traceNumberDao;
    private ScheduledPaymentDAO scheduledPaymentDao;
    private TransferTypeServiceLocal transferTypeService;
    private FetchServiceLocal fetchService;
    private LoggingHandler loggingHandler;
    private PermissionServiceLocal permissionService;
    private AlertServiceLocal alertService;
    private MessageResolver messageResolver;
    private PaymentCustomFieldServiceLocal paymentCustomFieldService;
    private RateServiceLocal rateService;
    private MemberNotificationHandler memberNotificationHandler;
    private AdminNotificationHandler adminNotificationHandler;
    private TransactionHelper transactionHelper;
    private ApplicationServiceLocal applicationService;
    private LockHandlerFactory lockHandlerFactory;
    private PaymentHelper paymentHelper;
    private AccountHelper accountHelper;
    private CustomObjectHandler customObjectHandler;

    private static final Log LOG = LogFactory.getLog(PaymentServiceImpl.class);

    @Override
    public List<BulkChargebackResult> bulkChargeback(final List<Transfer> transfers) {

        return transactionHelper.runInNewTransaction(new Transactional<List<BulkChargebackResult>>() {
            @Override
            public List<BulkChargebackResult> afterCommit(final List<BulkChargebackResult> result) {
                // Make sure all transfers are attached to the current session
                for (BulkChargebackResult bulkChargebackResult : result) {
                    bulkChargebackResult.setTransfer(fetchService.fetch(bulkChargebackResult.getTransfer()));
                }
                return result;
            }

            @Override
            public List<BulkChargebackResult> doInTransaction(final TransactionStatus status) {
                List<BulkChargebackResult> results = new ArrayList<BulkChargebackResult>(transfers.size());
                try {
                    for (Transfer transfer : transfers) {
                        if (transfer == null) {
                            results.add(null);
                        } else {
                            Transfer chargeback = insertChargeback(transfer, false);
                            results.add(new BulkChargebackResult(chargeback));
                        }
                    }
                } catch (ApplicationException e) {
                    results.add(new BulkChargebackResult(e));
                    status.setRollbackOnly();
                }
                return results;
            }
        });
    }

    @Override
    public List<ScheduledPaymentDTO> calculatePaymentProjection(final ProjectionDTO params) {
        getProjectionValidator().validate(params);

        final LocalSettings localSettings = settingsService.getLocalSettings();

        final int paymentCount = params.getPaymentCount();
        final TimePeriod recurrence = params.getRecurrence();
        final BigDecimal totalAmount = params.getAmount();
        final BigDecimal paymentAmount = localSettings.round(totalAmount
                .divide(CoercionHelper.coerce(BigDecimal.class, paymentCount), localSettings.getMathContext()));
        BigDecimal accumulatedAmount = BigDecimal.ZERO;
        Calendar currentDate = DateHelper.truncate(params.getFirstExpirationDate());
        final List<ScheduledPaymentDTO> payments = new ArrayList<ScheduledPaymentDTO>(paymentCount);
        for (int i = 0; i < paymentCount; i++) {
            final ScheduledPaymentDTO dto = new ScheduledPaymentDTO();
            dto.setDate(currentDate);
            dto.setAmount(i == paymentCount - 1 ? totalAmount.subtract(accumulatedAmount) : paymentAmount);
            payments.add(dto);
            accumulatedAmount = accumulatedAmount.add(dto.getAmount(), localSettings.getMathContext());
            currentDate = recurrence.add(currentDate);
        }
        return payments;
    }

    @Override
    public boolean canChargeback(Transfer transfer, final boolean ignorePendingPayment) {
        transfer = fetchService.fetch(transfer, Payment.Relationships.FROM, Payment.Relationships.TO,
                Transfer.Relationships.PARENT, Transfer.Relationships.CHARGEBACK_OF,
                Transfer.Relationships.CHILDREN);
        if (transfer == null) {
            return false;
        }
        // Pending payments cannot be charged back
        final Calendar processDate = transfer.getProcessDate();
        if (!ignorePendingPayment && processDate == null) {
            return false;
        }

        // Check the max chargeback time
        final LocalSettings localSettings = settingsService.getLocalSettings();
        final TimePeriod maxChargebackTime = localSettings.getMaxChargebackTime();
        final Calendar maxDate = maxChargebackTime.add(processDate);
        if (Calendar.getInstance().after(maxDate)) {
            return false;
        }

        // Nested transfers cannot be charged back
        if (transfer.getParent() != null) {
            return false;
        }
        // Payments which has already been charged back cannot be charged back again
        if (transfer.getChargedBackBy() != null) {
            return false;
        }
        // Payments which are chargebacks cannot be charged back
        if (transfer.getChargebackOf() != null) {
            return false;
        }
        // Cannot chargeback if from owner is removed
        if (!transfer.isFromSystem()) {
            final Member fromOwner = (Member) transfer.getFromOwner();
            if (fromOwner.getGroup().getStatus() == Group.Status.REMOVED) {
                return false;
            }
        }
        // Cannot chargeback if to owner is removed
        if (!transfer.isToSystem()) {
            final Member toOwner = (Member) transfer.getToOwner();
            if (toOwner.getGroup().getStatus() == Group.Status.REMOVED) {
                return false;
            }
        }
        return true;
    }

    /**
     * To perform a payment, the logged user must either manage the from owner or to owner and must be allowed<br>
     * to use the specified transfer type (if any).
     */
    @Override
    public boolean canMakePayment(AccountOwner from, final AccountOwner to, final TransferType transferType) {
        if (LoggedUser.isSystem()) {
            return true;
        }
        boolean checkToMember = false;
        boolean hasPermission = false;
        if (from == null) {
            from = LoggedUser.accountOwner();
        }
        if (from instanceof SystemAccountOwner) {
            if (to instanceof SystemAccountOwner) {
                // System to system payment
                hasPermission = permissionService.permission().admin(AdminSystemPermission.PAYMENTS_PAYMENT)
                        .hasPermission();
            } else {
                // System to member payment
                if (transferType == null) {
                    // No information about the TT. Can be either loan or payment
                    hasPermission = permissionService.permission()
                            .admin(AdminMemberPermission.PAYMENTS_PAYMENT, AdminMemberPermission.LOANS_GRANT)
                            .hasPermission();
                } else {
                    // We know whether is a loan type or payment type: check the specific permission
                    AdminMemberPermission permission = transferType.isLoanType() ? AdminMemberPermission.LOANS_GRANT
                            : AdminMemberPermission.PAYMENTS_PAYMENT;
                    hasPermission = permissionService.permission().admin(permission).hasPermission();
                }
                checkToMember = true;
            }
        } else {
            Member member = (Member) from;
            if (from.equals(to)) {
                // Member self payment
                hasPermission = permissionService.permission(member)
                        .admin(AdminMemberPermission.PAYMENTS_PAYMENT_AS_MEMBER_TO_SELF)
                        .broker(BrokerPermission.MEMBER_PAYMENTS_PAYMENT_AS_MEMBER_TO_SELF)
                        .member(MemberPermission.PAYMENTS_PAYMENT_TO_SELF)
                        .operator(OperatorPermission.PAYMENTS_PAYMENT_TO_SELF).hasPermission();
            } else if (to instanceof SystemAccountOwner) {
                // Member to system
                hasPermission = permissionService.permission(member)
                        .admin(AdminMemberPermission.PAYMENTS_PAYMENT_AS_MEMBER_TO_SYSTEM)
                        .broker(BrokerPermission.MEMBER_PAYMENTS_PAYMENT_AS_MEMBER_TO_SYSTEM)
                        .member(MemberPermission.PAYMENTS_PAYMENT_TO_SYSTEM)
                        .operator(OperatorPermission.PAYMENTS_PAYMENT_TO_SYSTEM).hasPermission();
            } else {
                // Member to member
                hasPermission = permissionService.permission(member)
                        .admin(AdminMemberPermission.PAYMENTS_PAYMENT_AS_MEMBER_TO_MEMBER)
                        .broker(BrokerPermission.MEMBER_PAYMENTS_PAYMENT_AS_MEMBER_TO_MEMBER)
                        .member(MemberPermission.PAYMENTS_PAYMENT_TO_MEMBER)
                        .operator(OperatorPermission.PAYMENTS_PAYMENT_TO_MEMBER,
                                OperatorPermission.PAYMENTS_POSWEB_MAKE_PAYMENT)
                        .hasPermission();
                checkToMember = true;
            }
        }

        if (hasPermission && transferType != null) {
            Collection<TransferType> allowedTypes = Collections.emptyList();
            // checks if the specified TT can be used
            if (LoggedUser.accountOwner().equals(from)) {
                Group group = fetchService.fetch(
                        LoggedUser.isOperator() ? LoggedUser.member().getGroup() : LoggedUser.group(),
                        Group.Relationships.TRANSFER_TYPES);
                allowedTypes = group.getTransferTypes();
            } else if (LoggedUser.isBroker()) {
                BrokerGroup brokerGroup = fetchService.fetch((BrokerGroup) LoggedUser.group(),
                        BrokerGroup.Relationships.TRANSFER_TYPES_AS_MEMBER);
                allowedTypes = brokerGroup.getTransferTypesAsMember();
            } else if (LoggedUser.isAdministrator()) {
                AdminGroup admGroup = fetchService.fetch((AdminGroup) LoggedUser.group(),
                        AdminGroup.Relationships.TRANSFER_TYPES_AS_MEMBER);
                allowedTypes = admGroup.getTransferTypesAsMember();
            }

            hasPermission = allowedTypes.contains(transferType);
        }

        // Besides, if the payment is to a member, ensure he is visible
        if (hasPermission && checkToMember) {
            return permissionService.relatesTo((Member) to);
        } else {
            return hasPermission;
        }
    }

    @Override
    public Transfer chargeback(final Transfer transfer) throws UnexpectedEntityException {
        if (!canChargeback(transfer, false)) {
            throw new UnexpectedEntityException();
        }

        // Insert the chargeback
        return insertChargeback(transfer, true);
    }

    @Override
    public Transfer conciliate(Transfer transfer, final ExternalTransfer externalTransfer) {
        transfer = fetchService.fetch(transfer, CONCILIATION_FETCH);
        if (transfer != null && transfer.getExternalTransfer() != null) {
            // If the transfer is already conciliated, ignore it
            transfer = null;
        }
        if (transfer != null) {
            final Account from = transfer.getFrom();
            final AccountOwner owner = from.getOwner();
            if (!owner.equals(externalTransfer.getMember())) {
                // The account does not belong to the expected member, ignore it
                transfer = null;
            }
        }
        if (transfer == null) {
            throw new UnexpectedEntityException();
        }
        return transferDao.updateExternalTransfer(transfer.getId(), externalTransfer);
    }

    @Override
    public Transfer confirmPayment(final String ticketStr)
            throws NotEnoughCreditsException, MaxAmountPerDayExceededException, EntityNotFoundException,
            UpperCreditLimitReachedException, AuthorizedPaymentInPastException {
        // Get and validate the ticket
        final PaymentRequestTicket ticket = ticketService.loadPendingPaymentRequest(ticketStr);
        final Member fromMember = ticket.getFrom();
        final Member toMember = ticket.getTo();
        final String channel = ticket.getToChannel().getInternalName();
        final String description = ticket.getDescription();

        // Create the payment
        final TransferDTO dto = new TransferDTO();
        dto.setFromOwner(fromMember);
        dto.setToOwner(toMember);
        dto.setTransferType(ticket.getTransferType());
        dto.setAmount(ticket.getAmount());
        dto.setChannel(channel);
        dto.setTicket(ticket);
        dto.setDescription(description);
        final Transfer transfer = (Transfer) insert(dto, true, false);

        // Update the ticket
        ticket.setStatus(Ticket.Status.OK);
        ticket.setTransfer(transfer);

        // Notify
        memberNotificationHandler.externalChannelPaymentConfirmed(ticket);

        return transfer;
    }

    @Override
    public List<BulkPaymentResult> doBulkPayment(final List<DoPaymentDTO> dtos) {
        return transactionHelper.runInNewTransaction(new Transactional<List<BulkPaymentResult>>() {
            @Override
            public List<BulkPaymentResult> afterCommit(final List<BulkPaymentResult> result) {
                // Make sure all transfers are attached to the current session
                for (BulkPaymentResult bulkPaymentResult : result) {
                    bulkPaymentResult.setPayment(fetchService.fetch(bulkPaymentResult.getPayment()));
                }
                return result;
            }

            @Override
            public List<BulkPaymentResult> doInTransaction(final TransactionStatus status) {
                List<BulkPaymentResult> results = new ArrayList<BulkPaymentResult>(dtos.size());
                try {
                    for (DoPaymentDTO dto : dtos) {
                        Payment payment = doPayment(dto, false, true, false);
                        results.add(new BulkPaymentResult(payment));
                    }
                } catch (ApplicationException e) {
                    results.add(new BulkPaymentResult(e));
                    status.setRollbackOnly();
                }
                return results;
            }
        });
    }

    @Override
    public Payment doPayment(final DoPaymentDTO params) {
        return doPayment(params, true, true, false);
    }

    @Override
    public AccountHistoryResultPage getAccountHistoryResultPage(final AccountHistoryParams params) {
        TransferQuery query = paymentHelper.toTransferQuery(params);
        List<Transfer> transfers = search(query);
        AccountHistoryResultPage result = accountHelper.toAccountHistoryResultPage(query.getOwner(), transfers);
        // Get the account status if requested
        if (params.getShowStatus()) {
            AccountStatusVO statusVO = accountService
                    .getCurrentAccountStatusVO(new AccountDTO(query.getOwner(), query.getType()));
            result.setAccountStatus(statusVO);
        }
        return result;
    }

    @Override
    public AccountHistoryTransferVO getAccountHistoryTransferVO(final Long id) {
        Transfer transfer = load(id);
        List<PaymentCustomField> fields = paymentCustomFieldService.list(transfer.getType(), false);
        return accountHelper.toVO(LoggedUser.member(), transfer, fields, null, null);
    }

    @Override
    public ConversionSimulationDTO getDefaultConversionDTO(MemberAccount account,
            final List<TransferType> transferTypes) {
        account = fetchService.fetch(account, Account.Relationships.TYPE, MemberAccount.Relationships.MEMBER);
        // Get the current account status
        final AccountStatus status = accountService.getRatedStatus(account, null);

        final ConversionSimulationDTO dto = new ConversionSimulationDTO();
        dto.setAccount(account);

        // Find the default amount: the balance of the current account
        BigDecimal defaultAmount = status.getAvailableBalanceWithoutCreditLimit();
        if (BigDecimal.ZERO.compareTo(defaultAmount) > 0) {
            defaultAmount = BigDecimal.ZERO;
        }
        dto.setAmount(defaultAmount);

        // find the first rated TT, and choose this.
        for (final TransferType currentTT : transferTypes) {
            if (currentTT.isHavingRatedFees()) {
                dto.setTransferType(currentTT);
                break;
            }
        }
        // If not any rated TT, just choose the first
        if (dto.getTransferType() == null) {
            dto.setTransferType(transferTypes.get(0));
        }

        dto.setDate(Calendar.getInstance());
        // erase any present content
        dto.setArate(null);
        dto.setDrate(null);
        // rates on a zero balance are meaningless, so...
        if (dto.getTransferType().isHavingRatedFees() && BigDecimal.ZERO.compareTo(defaultAmount) < 0) {
            if (dto.getTransferType().isHavingAratedFees()) {
                final BigDecimal aRate = status.getaRate();
                dto.setArate(aRate);
            }
            if (dto.getTransferType().isHavingDratedFees()) {
                final BigDecimal dRate = status.getdRate();
                dto.setDrate(dRate);
            }
        }

        return dto;
    }

    @Override
    public BigDecimal getMinimumPayment() {
        final LocalSettings localSettings = settingsService.getLocalSettings();
        final int precision = localSettings.getPrecision().getValue();
        final BigDecimal minimumPayment = new BigDecimal(new BigInteger("1"), precision);
        return minimumPayment;
    }

    @Override
    public StatisticalResultDTO getSimulateConversionGraph(final ConversionSimulationDTO input) {
        final LocalSettings localSettings = settingsService.getLocalSettings();
        final byte precision = (byte) localSettings.getPrecision().getValue();

        // get series
        final TransactionFeePreviewForRatesDTO temp = simulateConversion(input);
        final int series = temp.getFees().size();
        // get range of points, but without values for A < 0
        BigDecimal initialARate = null;
        RatesResultDTO rates = new RatesResultDTO();
        if (input.isUseActualRates()) {
            rates = rateService.getRatesForTransferFrom(input.getAccount(), input.getAmount(), null);
            rates.setDate(input.getDate());
            initialARate = rates.getaRate();
        } else {
            initialARate = input.getArate();
        }

        // lowerlimit takes care that values for A < 0 are left out of the graph
        final Double lowerLimit = (initialARate == null) ? null : initialARate.negate().doubleValue();
        final Number[] xRange = GraphHelper.getOptimalRangeAround(0, 33, 0, 0.8, lowerLimit);

        // Data structure to build the table
        final Number[][] tableCells = new Number[xRange.length][series];
        // initialize series names and x labels
        final String[] seriesNames = new String[series];
        final byte[] seriesOrder = new byte[series];
        final Calendar[] xPointDates = new Calendar[xRange.length];
        final Calendar now = Calendar.getInstance();
        BigDecimal inputARate = temp.getARate();
        BigDecimal inputDRate = temp.getDRate();
        // assign data
        for (int i = 0; i < xRange.length; i++) {
            final ConversionSimulationDTO inputPointX = (ConversionSimulationDTO) input.clone();
            final Calendar date = (Calendar) ((input.isUseActualRates()) ? input.getDate().clone() : now.clone());
            date.add(Calendar.DAY_OF_YEAR, xRange[i].intValue());
            xPointDates[i] = date;
            // Set useActualRates for this input to false, otherwise simulateConversion will use the account's the balance and rates of that date, and
            // we don't want that.
            inputPointX.setUseActualRates(false);
            if (inputARate != null) {
                final BigDecimal aRate = inputARate.add(new BigDecimal(xRange[i].doubleValue()));
                inputPointX.setArate(aRate);
            }
            if (inputDRate != null) {
                final BigDecimal dRate = inputDRate.subtract(new BigDecimal(xRange[i].doubleValue()));
                inputPointX.setDrate(dRate);
            }

            final TransactionFeePreviewDTO tempResult = simulateConversion(inputPointX);
            int j = 0;
            for (final TransactionFee fee : tempResult.getFees().keySet()) {
                tableCells[i][j] = new StatisticalNumber(tempResult.getFees().get(fee).doubleValue(), precision);
                byte index;
                switch (fee.getChargeType()) {
                case D_RATE:
                    index = 2;
                    break;
                case A_RATE:
                case MIXED_A_D_RATES:
                    index = 3;
                    break;
                default:
                    index = 1;
                    break;
                }
                seriesOrder[j] = index;
                seriesNames[j++] = fee.getName();
            }
        }

        // create the graph object
        final StatisticalResultDTO result = new StatisticalResultDTO(tableCells);
        result.setBaseKey("conversionSimulation.result.graph");
        result.setHelpFile("account_management");
        // date labels along x-axis
        final String[] rowKeys = new String[xRange.length];
        Arrays.fill(rowKeys, "");
        result.setRowKeys(rowKeys);
        for (int i = 0; i < rowKeys.length; i++) {
            final String rowHeader = localSettings.getDateConverterForGraphs().toString(xPointDates[i]);
            result.setRowHeader(rowHeader, i);
        }
        // mark the actual date upon which the x-axis is based as a vertical line
        final Calendar baseDate = (input.isUseActualRates()) ? (Calendar) input.getDate().clone() : now;
        final String baseDateString = localSettings.getDateConverterForGraphs().toString(baseDate);
        final Marker[] markers = new Marker[1];
        markers[0] = new CategoryMarker(baseDateString);
        markers[0].setPaint(Color.ORANGE);
        final String todayString = localSettings.getDateConverterForGraphs().toString(now);
        if (todayString.equals(baseDateString)) {
            markers[0].setLabel("global.today");
        }
        result.setDomainMarkers(markers);

        // Series labels indicate fee names
        final String[] columnKeys = new String[series];
        Arrays.fill(columnKeys, "");
        result.setColumnKeys(columnKeys);
        for (int j = 0; j < columnKeys.length; j++) {
            result.setColumnHeader(seriesNames[j], j);
        }

        // order the series
        result.orderSeries(seriesOrder);

        final TransferType tt = fetchService.fetch(input.getTransferType(),
                RelationshipHelper.nested(TransferType.Relationships.FROM, AccountType.Relationships.CURRENCY));
        result.setYAxisUnits(tt.getCurrency().getSymbol());
        result.setShowTable(false);
        result.setGraphType(StatisticalResultDTO.GraphType.STACKED_AREA);
        return result;
    }

    @Override
    public boolean hasPermissionsToChargeback(Transfer transfer) {
        transfer = fetchService.fetch(transfer,
                RelationshipHelper.nested(Payment.Relationships.TO, MemberAccount.Relationships.MEMBER));
        // If it's a payment from member.. check it's related
        boolean isFromMember = !transfer.isFromSystem();
        if (isFromMember) {
            if (!permissionService.relatesTo((Member) transfer.getFromOwner())) {
                return false;
            }
        }
        if (transfer.isToSystem()) {
            // Payment to system
            return permissionService.permission()
                    .adminFor(AdminSystemPermission.PAYMENTS_CHARGEBACK, transfer.getType()).hasPermission();
        } else {
            // Payment to member
            return permissionService.permission((Member) transfer.getToOwner())
                    .adminFor(AdminMemberPermission.PAYMENTS_CHARGEBACK, transfer.getType())
                    .memberFor(MemberPermission.PAYMENTS_CHARGEBACK, transfer.getType()).hasPermission();
        }
    }

    @Override
    public Payment insertWithNotification(final TransferDTO dto) throws NotEnoughCreditsException,
            MaxAmountPerDayExceededException, UnexpectedEntityException, UpperCreditLimitReachedException {
        Payment payment = insert(dto, false, false);
        if (payment instanceof Transfer) {
            memberNotificationHandler.automaticPaymentReceivedNotification((Transfer) payment, dto);
        }
        return payment;
    }

    @Override
    public Payment insertWithoutNotification(final TransferDTO dto) throws NotEnoughCreditsException,
            MaxAmountPerDayExceededException, UnexpectedEntityException, UpperCreditLimitReachedException {
        return insert(dto, false, false);
    }

    @Override
    public boolean isVisible(Payment payment) {
        if (LoggedUser.isSystem()) {
            return true;
        }
        if (payment instanceof Transfer && LoggedUser.isOperator()) {
            // An operator should be able to view transfers he has received
            Transfer transfer = (Transfer) payment;
            if (LoggedUser.element().equals(transfer.getReceiver())
                    || LoggedUser.element().equals(transfer.getBy())) {
                return true;
            }
        }
        if (payment instanceof Transfer) {
            payment = fetchService.fetch((Transfer) payment, Payment.Relationships.FROM, Payment.Relationships.TO);
        } else {
            payment = fetchService.fetch((ScheduledPayment) payment, Payment.Relationships.FROM,
                    Payment.Relationships.TO);
        }
        return accountService.canView(payment.getFrom()) || accountService.canView(payment.getTo());
    }

    @Override
    public Transfer load(final Long id, final Relationship... fetch) {
        return transferDao.<Transfer>load(id, fetch);
    }

    @Override
    public Transfer loadTransferForReverse(final String traceNumber, final Relationship... fetch)
            throws EntityNotFoundException {
        long clientId = LoggedUser.serviceClient().getId();
        Transfer transfer = transferDao.loadTransferByTraceNumber(traceNumber, clientId);
        if (transfer == null) {
            if (!insertTN(LoggedUser.serviceClient().getId(), traceNumber)) {
                // the TN already exists
                transfer = transferDao.loadTransferByTraceNumber(traceNumber, clientId);
            }
            if (transfer == null) {
                throw new EntityNotFoundException(Transfer.class, null, String
                        .format("TraceNumber and client id used to load: <%1$s, %2$s>", traceNumber, clientId));
            }
        }
        return transfer;
    }

    @Override
    public void notifyTransferProcessed(final Transfer transfer) {
        if (transfer.getProcessDate() == null) {
            // Transfer is not processed
            return;
        }

        final Collection<TransferListener> listeners = getTransferListeners(transfer);
        if (CollectionUtils.isNotEmpty(listeners)) {
            CurrentTransactionData.addTransactionCommitListener(new TransactionCommitListener() {
                @Override
                public void onTransactionCommit() {
                    transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() {
                        @Override
                        protected void doInTransactionWithoutResult(final TransactionStatus status) {
                            Transfer fetchedTransfer = fetchService.fetch(transfer, Payment.Relationships.FROM,
                                    Payment.Relationships.TO);
                            for (TransferListener listener : listeners) {
                                try {
                                    listener.onTransferProcessed(fetchedTransfer);
                                } catch (Exception e) {
                                    LOG.warn("Error running TransferListener " + listener, e);
                                }
                            }
                        }
                    });
                }
            });
        }
    }

    @Override
    public void processScheduled(final Period period) {
        // Process each transfer
        final TransferQuery query = new TransferQuery();
        query.setResultType(ResultType.ITERATOR);
        query.setPeriod(period);
        query.setStatus(Payment.Status.SCHEDULED);
        query.setUnordered(true);
        CacheCleaner cacheCleaner = new CacheCleaner(fetchService);
        final List<Transfer> transfers = transferDao.search(query);
        try {
            for (final Transfer transfer : transfers) {
                processScheduledTransfer(transfer, true, true, true);
                cacheCleaner.clearCache();
            }
        } finally {
            DataIteratorHelper.close(transfers);
        }
    }

    @Override
    public Transfer processScheduled(final Transfer transfer) {
        return processScheduledTransfer(transfer, false, false, true);
    }

    @Override
    public void purgeOldTraceNumbers(final Calendar time) {
        Calendar c = (Calendar) time.clone();
        c.add(Calendar.DAY_OF_MONTH, -1);

        traceNumberDao.delete(c);
    }

    @Override
    public List<Transfer> search(final TransferQuery query) {
        return transferDao.search(query);
    }

    public void setAccountHelper(final AccountHelper accountHelper) {
        this.accountHelper = accountHelper;
    }

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

    public void setAdminNotificationHandler(final AdminNotificationHandler adminNotificationHandler) {
        this.adminNotificationHandler = adminNotificationHandler;
    }

    public void setAlertServiceLocal(final AlertServiceLocal alertService) {
        this.alertService = alertService;
    }

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

    public void setCommissionServiceLocal(final CommissionServiceLocal commissionService) {
        this.commissionService = commissionService;
    }

    public void setCustomObjectHandler(final CustomObjectHandler customObjectHandler) {
        this.customObjectHandler = customObjectHandler;
    }

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

    public void setLockHandlerFactory(final LockHandlerFactory lockHandlerFactory) {
        this.lockHandlerFactory = lockHandlerFactory;
    }

    public void setLoggingHandler(final LoggingHandler loggingHandler) {
        this.loggingHandler = loggingHandler;
    }

    public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) {
        this.memberNotificationHandler = memberNotificationHandler;
    }

    public void setMessageResolver(final MessageResolver messageResolver) {
        this.messageResolver = messageResolver;
    }

    public void setPaymentCustomFieldServiceLocal(final PaymentCustomFieldServiceLocal paymentCustomFieldService) {
        this.paymentCustomFieldService = paymentCustomFieldService;
    }

    public void setPaymentHelper(final PaymentHelper paymentHelper) {
        this.paymentHelper = paymentHelper;
    }

    public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) {
        this.permissionService = permissionService;
    }

    public void setRateServiceLocal(final RateServiceLocal rateService) {
        this.rateService = rateService;
    }

    public void setScheduledPaymentDao(final ScheduledPaymentDAO scheduledPaymentDao) {
        this.scheduledPaymentDao = scheduledPaymentDao;
    }

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

    public void setTicketServiceLocal(final TicketServiceLocal ticketService) {
        this.ticketService = ticketService;
    }

    public void setTraceNumberDao(final TraceNumberDAO traceNumberDao) {
        this.traceNumberDao = traceNumberDao;
    }

    public void setTransactionFeeServiceLocal(final TransactionFeeServiceLocal transactionFeeService) {
        this.transactionFeeService = transactionFeeService;
    }

    public void setTransactionHelper(final TransactionHelper transactionHelper) {
        this.transactionHelper = transactionHelper;
    }

    public void setTransferAuthorizationServiceLocal(
            final TransferAuthorizationServiceLocal transferAuthorizationService) {
        this.transferAuthorizationService = transferAuthorizationService;
    }

    public void setTransferDao(final TransferDAO transferDao) {
        this.transferDao = transferDao;
    }

    public void setTransferTypeServiceLocal(final TransferTypeServiceLocal transferTypeService) {
        this.transferTypeService = transferTypeService;
    }

    @Override
    public TransactionFeePreviewForRatesDTO simulateConversion(final ConversionSimulationDTO params) {
        TransferType transferType = params.getTransferType();
        transferType = fetchService.fetch(transferType, TransferType.Relationships.TO,
                TransferType.Relationships.TRANSACTION_FEES,
                RelationshipHelper.nested(SystemAccountType.Relationships.ACCOUNT));
        final MemberAccount account = fetchService.fetch(params.getAccount(), Account.Relationships.TYPE,
                MemberAccount.Relationships.MEMBER);

        RatesPreviewDTO rates = new RatesPreviewDTO();
        if (params.isUseActualRates()) {
            rates = new RatesPreviewDTO(
                    rateService.getRatesForTransferFrom(account, params.getAmount(), params.getDate()));
        } else {
            if (transferType.isHavingAratedFees()) {
                rates.setaRate(params.getArate());
            }
            if (transferType.isHavingDratedFees()) {
                rates.setdRate(params.getDrate());
            }
            rates.setDate(params.getDate());
            rates = new RatesPreviewDTO(rateService.rateToDate(rates));
        }
        rates.setGraph(params.isGraph());

        final Member from = account.getMember();
        final BigDecimal amount = params.getAmount();
        final SystemAccountOwner to = SystemAccountOwner.instance();
        final TransactionFeePreviewForRatesDTO preview = (TransactionFeePreviewForRatesDTO) transactionFeeService
                .preview(from, to, transferType, amount, rates);
        return preview;
    }

    @Override
    public Payment simulatePayment(final DoPaymentDTO params)
            throws NotEnoughCreditsException, MaxAmountPerDayExceededException, UnexpectedEntityException,
            UpperCreditLimitReachedException, AuthorizedPaymentInPastException {
        return transactionHelper.runInNewTransaction(new BaseTransactional<Payment>() {
            @Override
            public Payment doInTransaction(final TransactionStatus status) {
                status.setRollbackOnly();
                return doPayment(params, false, false, true);
            }
        });
    }

    @Override
    public void validate(final ConversionSimulationDTO dto) {
        final Validator validator = new Validator("");
        validator.property("amount").key("conversionSimulation.amount").required().positiveNonZero();
        if (dto.isUseActualRates()) {
            validator.property("date").key("conversionSimulation.date").required();
        } else {
            final Account account = fetchService.fetch(dto.getAccount(),
                    RelationshipHelper.nested(Account.Relationships.TYPE, AccountType.Relationships.CURRENCY));
            final TransferType transferType = fetchService.fetch(dto.getTransferType(),
                    TransferType.Relationships.TRANSACTION_FEES);
            final Currency currency = account.getType().getCurrency();
            if (currency.isEnableARate() && transferType.isHavingAratedFees()) {
                validator.property("arate").key("conversionSimulation.aRate.targeted").required().positive();
            }
            if (currency.isEnableDRate() && transferType.isHavingDratedFees()) {
                validator.property("drate").key("conversionSimulation.dRate.targeted").required();
            }
        }
        validator.validate(dto);
    }

    @Override
    public void validate(final DoPaymentDTO payment) {
        getPaymentValidator(payment).validate(payment);
    }

    /**
     * Validates the max amount per day
     */
    @Override
    public void validateMaxAmountAtDate(final Calendar date, final Account account, final TransferType transferType,
            BigDecimal maxAmountPerDay, final BigDecimal amount) {
        // Test the max amount per day
        maxAmountPerDay = maxAmountPerDay == null ? transferType.getMaxAmountPerDay() : maxAmountPerDay;
        if (maxAmountPerDay != null && maxAmountPerDay.floatValue() > PRECISION_DELTA) {
            // Get the amount on today
            BigDecimal amountOnDay = transferDao.getTransactionedAmountAt(date, account, transferType);

            // Validate
            if (amountOnDay.add(amount).compareTo(maxAmountPerDay) > 0) {
                throw new MaxAmountPerDayExceededException(date, transferType, account, amount);
            }
        }

        // Test the operator max amount per day
        if (LoggedUser.hasUser() && LoggedUser.isOperator()) {
            final Operator operator = LoggedUser.element();
            OperatorGroup group = operator.getOperatorGroup();
            group = fetchService.fetch(group, OperatorGroup.Relationships.MAX_AMOUNT_PER_DAY_BY_TRANSFER_TYPE);
            final BigDecimal maxAmount = group.getMaxAmountPerDayByTransferType().get(transferType);
            if (maxAmount != null && maxAmount.floatValue() > PRECISION_DELTA) {
                // Get the amount on today
                BigDecimal amountOnDay = transferDao.getTransactionedAmountAt(date, operator, account,
                        transferType);
                // Validate
                if (amountOnDay.add(amount).compareTo(maxAmount) == 1) {
                    throw new MaxAmountPerDayExceededException(date, transferType, account, amount);
                }
            }
        }
    }

    @Override
    public boolean wouldRequireAuthorization(final DoPaymentDTO params) {
        AuthorizationLevel firstAuthorizationLevel = null;
        // Scheduled payments shouldn't be authorized, only it's payments
        if (CollectionUtils.isEmpty(params.getPayments())) {
            firstAuthorizationLevel = firstAuthorizationLevel(params.getTransferType(), params.getAmount(),
                    params.getFrom());
        }
        return firstAuthorizationLevel != null;
    }

    @Override
    public boolean wouldRequireAuthorization(final Invoice invoice) {
        final DoPaymentDTO payment = new DoPaymentDTO();
        payment.setFrom(invoice.getTo());
        payment.setTo(invoice.getFrom());
        payment.setTransferType(invoice.getTransferType());
        payment.setAmount(invoice.getAmount());
        return wouldRequireAuthorization(payment);
    }

    @Override
    public boolean wouldRequireAuthorization(final Transfer transfer) {
        return firstAuthorizationLevel(transfer) != null;
    }

    @Override
    public boolean wouldRequireAuthorization(final TransferType transferType, final BigDecimal amount,
            final AccountOwner from) {
        return firstAuthorizationLevel(transferType, amount, from) != null;
    }

    private void addAmountValidator(final Validator validator, final TransferType tt) {
        Property amountProperty = validator.property("amount").required().positiveNonZero();

        // Max amount & min amount
        if (tt != null && tt.getMinAmount() != null) {
            amountProperty.greaterEquals(tt.getMinAmount());
        }

    }

    /**
     * Runs the {@link #performInsert(TransferDTO, AuthorizationLevel)} method optionally in a new transaction. Does this while deadlocks occur. Other
     * errors are just rethrown.
     */
    private Payment doInsert(final TransferDTO dto, final boolean newTransaction, final boolean simulation) {
        return transactionHelper.maybeRunInNewTransaction(new Transactional<Payment>() {
            @Override
            public Payment afterCommit(final Payment payment) {
                return fetchService.fetch(payment);
            }

            @Override
            public Payment doInTransaction(final TransactionStatus status) {
                return performInsert(dto, simulation);
            }
        }, newTransaction);
    }

    private Payment doPayment(final DoPaymentDTO params, final boolean newTransaction, final boolean notify,
            final boolean simulation) {
        // Check permission to pay with date
        if (params.getDate() != null
                && !permissionService.hasPermission(AdminMemberPermission.PAYMENTS_PAYMENT_WITH_DATE)) {
            throw new PermissionDeniedException();
        }

        // Validate dto
        validate(params);

        // Insert the transfer
        final TransferDTO dto = verify(params);

        final Payment payment = doInsert(dto, newTransaction, simulation);

        // Notify
        if (notify) {
            if (LoggedUser.isWebService()) {
                memberNotificationHandler.externalChannelPaymentPerformed(params, payment);
            } else {
                memberNotificationHandler.paymentReceivedNotification(payment);
            }
            if (payment instanceof Transfer) {
                Transfer transfer = (Transfer) payment;
                if (payment.getProcessDate() == null) {
                    adminNotificationHandler.notifyNewPendingPayment(transfer);
                } else {
                    adminNotificationHandler.notifyPayment(transfer);
                }
            }
        }
        return payment;
    }

    private Transfer doProcessScheduledTransfer(final LockHandler lockHandler, Transfer transfer,
            final boolean failOnError, final boolean notifyPayer, final boolean notifyReceiver) {
        transfer = fetchService.fetch(transfer, Transfer.Relationships.SCHEDULED_PAYMENT);
        ScheduledPayment scheduledPayment = transfer.getScheduledPayment();
        if (scheduledPayment == null || !transfer.getStatus().canPayNow()) {
            throw new UnexpectedEntityException();
        }

        final Account from = transfer.getFrom();
        final Account to = transfer.getTo();

        // Lock the required accounts
        LockedAccountsOnPayments lockedAccountsOnPayments = applicationService.getLockedAccountsOnPayments();
        if (lockedAccountsOnPayments == LockedAccountsOnPayments.ORIGIN) {
            lockHandler.lock(from);
        } else if (lockedAccountsOnPayments == LockedAccountsOnPayments.ALL) {
            lockHandler.lock(from, to);
        }

        // We have to refresh the transfer after locking, to make sure it's still possible to pay now
        transfer = fetchService.reload(transfer, Transfer.Relationships.SCHEDULED_PAYMENT);
        scheduledPayment = transfer.getScheduledPayment();
        if (!transfer.getStatus().canPayNow()) {
            throw new UnexpectedEntityException();
        }

        final BigDecimal amount = transfer.getAmount();
        final TransferType transferType = transfer.getType();
        final AuthorizationLevel firstAuthorizationLevel = firstAuthorizationLevel(transfer);
        try {
            Account fromAccountToValidate = from;
            if (scheduledPayment.isReserveAmount()) {
                // When the scheduled payment has reserved the amount, we don't need to validate the from amount, because it's guaranteed to have a
                // reserved amount, so, we pass fromAccount = null
                fromAccountToValidate = null;
            }
            Collection<TransferListener> listeners = getTransferListeners(transfer);

            // Notify the listeners before the amount is validated
            for (TransferListener listener : listeners) {
                listener.onBeforeValidateBalance(transfer);
            }

            validateAmount(amount, fromAccountToValidate, to, transfer);
            final TransactionFeePreviewDTO preview = transactionFeeService.preview(from.getOwner(), to.getOwner(),
                    transferType, amount);
            transfer.setAmount(preview.getFinalAmount());
            if (LoggedUser.hasUser()) {
                transfer.setBy(LoggedUser.element());
            }
            boolean shouldLiberateAmount = false;
            if (firstAuthorizationLevel != null) {
                transfer.setStatus(Transfer.Status.PENDING);
                transfer.setNextAuthorizationLevel(firstAuthorizationLevel);

                // Insert an amount reservation for this pending transfer, unless the scheduled payment has already reserved it
                if (!scheduledPayment.isReserveAmount()) {
                    accountService.reservePending(transfer);
                }
            } else {
                // apply rates
                RatesToSave rates = rateService.applyTransfer(transfer);
                /*
                 * set processDate AFTER applying rates, but before persisting them. This is important, because the transfer itself must not sum up
                 * for rates or balances when the rates are processed, and it does if processdate is already set. In that case, the transfer's
                 * processDate can equal the fromRates's date.
                 */
                Calendar processDate = (rates.getFromRates() == null) ? Calendar.getInstance()
                        : rates.getFromRates().getDate();
                transfer.setStatus(Transfer.Status.PROCESSED);
                transfer.setProcessDate(processDate);
                rateService.persist(rates);
                transfer.setEmissionDate(rates.getEmissionDate());
                transfer.setExpirationDate(rates.getExpirationDate());
                transfer.setiRate(rates.getiRate());
                shouldLiberateAmount = scheduledPayment.isReserveAmount();

                // Generate the transaction number
                final TransactionNumber transactionNumber = settingsService.getLocalSettings()
                        .getTransactionNumber();
                if (transactionNumber != null && transactionNumber.isValid()) {
                    final String generated = transactionNumber.generate(transfer.getId(),
                            transfer.getProcessDate());
                    transfer.setTransactionNumber(generated);
                }
            }

            // Notify the listeners before the payment is updated (may be seen as an insert)
            for (TransferListener listener : listeners) {
                listener.onTransferInserted(transfer);
            }

            transferDao.update(transfer);

            // Make sure no closed balances exist on the future, to fix eventual future closed balances
            accountService.removeClosedBalancesAfter(transfer.getFrom(), transfer.getProcessDate());
            accountService.removeClosedBalancesAfter(transfer.getTo(), transfer.getProcessDate());

            // Notify listeners - Should be before inserting fees to ensure the correct notification order
            notifyTransferProcessed(transfer);

            // Insert fees
            insertFees(lockHandler, transfer, false, amount, false, new HashSet<ChargedFee>());

            // Insert the corresponding amount reservation if the scheduled payment had reserved the total amount
            if (shouldLiberateAmount) {
                accountService.returnReservationForInstallment(transfer);
            }

            // Update scheduled payment status
            updateScheduledPaymentStatus(scheduledPayment);

            memberNotificationHandler.scheduledPaymentProcessingNotification(transfer, notifyPayer, notifyReceiver);
            // Notify admins
            if (transfer.getProcessDate() == null) {
                adminNotificationHandler.notifyNewPendingPayment(transfer);
            }

        } catch (final RuntimeException e) {
            if (failOnError) {
                transferDao.updateStatus(transfer.getId(), Payment.Status.FAILED);
                updateScheduledPaymentStatus(scheduledPayment);

                // Ensure the amount is liberated
                if (scheduledPayment.isReserveAmount()) {
                    accountService.returnReservationForInstallment(transfer);
                }

                memberNotificationHandler.scheduledPaymentProcessingNotification(transfer, notifyPayer,
                        notifyReceiver);

                // Generate an alert when it's from system
                if (transfer.isFromSystem()) {
                    final Member member = (Member) transfer.getToOwner();
                    final LocalSettings settings = settingsService.getLocalSettings();
                    final Object[] arguments = {
                            settings.getUnitsConverter(transfer.getType().getFrom().getCurrency().getPattern())
                                    .toString(transfer.getAmount()),
                            transfer.getType().getName() };
                    alertService.create(member, MemberAlert.Alerts.SCHEDULED_PAYMENT_FAILED, arguments);
                }
            } else {
                throw e;
            }
        }
        return transfer;
    }

    private Transfer doProcessScheduledTransfer(final Transfer transfer, final boolean failOnError,
            final boolean notifyPayer, final boolean notifyReceiver) {
        LockHandler lockHandler = lockHandlerFactory.getLockHandlerIfLockingAccounts();
        try {
            return doProcessScheduledTransfer(lockHandler, transfer, failOnError, notifyPayer, notifyReceiver);
        } finally {
            if (lockHandler != null) {
                lockHandler.release();
            }
        }
    }

    /**
     * Resolve the first authorization level for the given payment, if any. When the payment wouldn't be authorizable, return null
     */
    private AuthorizationLevel firstAuthorizationLevel(final Transfer transfer) {
        // If the transfer is an installment of a scheduled payment, when exists another installment which was already authorized, no new auth is
        // needed
        if (transfer.getScheduledPayment() != null) {
            for (Transfer installment : transfer.getScheduledPayment().getTransfers()) {
                if (installment.getProcessDate() != null) {
                    // A processed installment. No further authorization needed
                    return null;
                }
            }
        }
        return firstAuthorizationLevel(transfer.getType(), transfer.getAmount(), transfer.getFromOwner());

    }

    /**
     * Resolve the first authorization level for the given payment, if any. When the payment wouldn't be authorizable, return null
     */
    private AuthorizationLevel firstAuthorizationLevel(TransferType transferType, final BigDecimal amount,
            AccountOwner from) {
        transferType = fetchService.fetch(transferType, TransferType.Relationships.AUTHORIZATION_LEVELS);
        if (transferType.isRequiresAuthorization()
                && CollectionUtils.isNotEmpty(transferType.getAuthorizationLevels())) {
            if (from == null) {
                from = LoggedUser.accountOwner();
            }
            final Account account = accountService.getAccount(new AccountDTO(from, transferType.getFrom()));
            BigDecimal amountSoFarToday = transferDao.getTransactionedAmountAt(null, account, transferType);
            final AuthorizationLevel authorization = transferType.getAuthorizationLevels().iterator().next();

            // When the amount is greater than the authorization, return true
            final BigDecimal amountToTest = amountSoFarToday.add(amount);
            if (amountToTest.compareTo(authorization.getAmount()) >= 0) {
                return transferType.getAuthorizationLevels().iterator().next();
            }
        }
        return null;
    }

    private Validator getPaymentValidator(final DoPaymentDTO payment) {
        final Validator validator = new Validator("transfer");
        Collection<TransactionContext> possibleContexts = new ArrayList<TransactionContext>();
        possibleContexts.add(TransactionContext.PAYMENT);
        if (LoggedUser.isWebService() || LoggedUser.isSystem()) {
            possibleContexts.add(TransactionContext.AUTOMATIC);
        } else {
            possibleContexts.add(TransactionContext.SELF_PAYMENT);
        }
        validator.property("context").required().anyOf(possibleContexts);
        validator.property("to").required().key("payment.recipient");
        // as currency is maybe not set on the DTO, we get it from the TT in stead of directly from the DTO
        final TransferType tt = fetchService.fetch(payment.getTransferType(), Relationships.TRANSACTION_FEES,
                RelationshipHelper.nested(TransferType.Relationships.FROM, TransferType.Relationships.TO,
                        AccountType.Relationships.CURRENCY, Currency.Relationships.A_RATE_PARAMETERS),
                RelationshipHelper.nested(TransferType.Relationships.FROM, TransferType.Relationships.TO,
                        AccountType.Relationships.CURRENCY, Currency.Relationships.D_RATE_PARAMETERS));
        final Currency currency = tt == null ? null : tt.getCurrency();
        if (currency != null && (currency.isEnableARate() || currency.isEnableDRate())) {
            // if the date is not null at this moment, it is in the past, which is not allowed with rates.
            if (payment.getDate() != null) {
                validator.general(new NoPastDateWithRatesValidator());
            }
        } else {
            validator.property("date").key("payment.manualDate").past();
        }

        validator.property("ticket").add(new TicketValidation());

        addAmountValidator(validator, tt);
        validator.property("transferType").key("transfer.type").required();
        validator.property("description").maxLength(1000);
        validator.general(new SchedulingValidator());
        validator.general(new PendingContractValidator());
        if (payment.getTransferType() != null && payment.getTo() != null && payment.getAmount() != null) {

            /*
             * For user validation, we need to check if the transaction amount is high enough to cover all fees. This depends on all fees, but only in
             * case of fixed fees it makes sense to increase the transaction amount. The formula for this is: given transactionamount > (sum of fixed
             * fees )/ (1 minus sum of percentage fees expressed as fractions). This of course only applies for fees with deductAmount; fees which are
             * not deducted are excluded from this calculation.
             */
            final TransactionFeePreviewDTO preview = transactionFeeService.preview(payment.getFrom(),
                    payment.getTo(), tt, payment.getAmount());
            final Property amount = validator.property("amount");
            final Collection<? extends TransactionFee> fees = preview.getFees().keySet();
            BigDecimal sumOfFixedFees = BigDecimal.ZERO;
            BigDecimal sumOfPercentageFees = BigDecimal.ZERO;
            for (final TransactionFee fee : fees) {
                if (fee.isDeductAmount()) {
                    if (fee.getChargeType() == ChargeType.FIXED) {
                        sumOfFixedFees = sumOfFixedFees.add(preview.getFees().get(fee));
                    } else {
                        sumOfPercentageFees = sumOfPercentageFees.add(preview.getFees().get(fee));
                    }
                }
            }
            // Show a warning if there are fixed fees and if the amount is not enough to cover them
            if (sumOfFixedFees.signum() == 1) {
                final int scale = LocalSettings.MAX_PRECISION;
                final MathContext mc = new MathContext(scale);
                final BigDecimal sumOfPercentages = sumOfPercentageFees.divide(payment.getAmount(), mc);
                final BigDecimal minimalAmount = sumOfFixedFees.divide((BigDecimal.ONE.subtract(sumOfPercentages)),
                        mc);
                amount.comparable(minimalAmount, ">", new ValidationError("errors.greaterThan",
                        messageResolver.message("transactionFee.invalidChargeValue", minimalAmount)));
            } else if (preview.getFinalAmount().signum() == -1) {
                validator.general(new FinalAmountValidator());
            }

            // Custom fields
            validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() {
                @Override
                public Validator getValidator() {
                    return paymentCustomFieldService.getValueValidator(payment.getTransferType());
                }
            }));
        }
        return validator;
    }

    private Validator getProjectionValidator() {
        final Validator projectionValidator = new Validator("transfer");
        projectionValidator.property("paymentCount").required().positiveNonZero().add(new PropertyValidation() {
            private static final long serialVersionUID = 5022911381764849941L;

            @Override
            public ValidationError validate(final Object object, final Object property, final Object value) {
                final Integer paymentCount = (Integer) value;
                if (paymentCount == null) {
                    return null;
                }
                final ProjectionDTO dto = (ProjectionDTO) object;
                final AccountOwner from = dto.getFrom();
                if (from instanceof Member) {
                    final Member member = fetchService.fetch((Member) from, Element.Relationships.GROUP);
                    final int maxSchedulingPayments = member.getMemberGroup().getMemberSettings()
                            .getMaxSchedulingPayments();
                    return CompareToValidation.lessEquals(maxSchedulingPayments).validate(object, property, value);
                }
                return null;
            }
        });
        projectionValidator.property("amount").required().positiveNonZero();
        projectionValidator.property("firstExpirationDate").key("transfer.firstPaymentDate").required()
                .add(new PropertyValidation() {
                    private static final long serialVersionUID = -3612786027250751763L;

                    @Override
                    public ValidationError validate(final Object object, final Object property,
                            final Object value) {
                        final Calendar firstDate = CoercionHelper.coerce(Calendar.class, value);
                        if (firstDate == null) {
                            return null;
                        }
                        if (firstDate.before(DateHelper.truncate(Calendar.getInstance()))) {
                            return new InvalidError();
                        }
                        return null;
                    }
                });
        projectionValidator.property("recurrence.number").key("transfer.paymentEvery").required().between(1, 100);
        projectionValidator.property("recurrence.field").key("transfer.paymentEvery").required()
                .anyOf(TimePeriod.Field.DAYS, TimePeriod.Field.WEEKS, TimePeriod.Field.MONTHS);
        return projectionValidator;
    }

    /**
     * gets the topmost parent of the transfer
     */
    private Transfer getTopMost(final Transfer transfer) {
        Transfer topMost = transfer;
        while (topMost.getParent() != null) {
            topMost = topMost.getParent();
        }
        return topMost;
    }

    private Collection<TransferListener> getTransferListeners(final Transfer transfer) {
        TransferType type = transfer.getType();
        Collection<TransferListener> result = new ArrayList<TransferListener>(2);

        // Get the listener from transfer type, if any
        if (StringUtils.isNotEmpty(type.getTransferListenerClass())) {
            TransferListener listener = customObjectHandler.get(type.getTransferListenerClass());
            result.add(listener);
        }

        // Get the listener from settings, if any
        LocalSettings settings = settingsService.getLocalSettings();
        if (StringUtils.isNotEmpty(settings.getTransferListenerClass())) {
            TransferListener listener = customObjectHandler.get(settings.getTransferListenerClass());
            result.add(listener);
        }
        return result;
    }

    private Validator getTransferValidator(final TransferDTO transfer) {
        final Validator validator = new Validator("transfer");
        // as currency is sometimes not set on the DTO, we get it from the TT in stead of directly from the DTO
        final TransferType tt = fetchService.fetch(transfer.getTransferType(),
                RelationshipHelper.nested(TransferType.Relationships.FROM, AccountType.Relationships.CURRENCY,
                        Currency.Relationships.A_RATE_PARAMETERS, Currency.Relationships.D_RATE_PARAMETERS));
        final Currency currency = tt.getCurrency();
        // if rates enabled, it is not allowed to have a date in the past.
        if (currency.isEnableARate() || currency.isEnableDRate()) {
            final Calendar now = Calendar.getInstance();
            // make a few minutes earlier, because if the transfer's date has just before been set to Calendar.getInstance(), it may already be a
            // few milliseconds or even seconds later.
            now.add(Calendar.MINUTE, -4);
            final Calendar date = transfer.getDate();
            if (date != null && date.before(now)) {
                validator.general(new NoPastDateWithRatesValidator());
            }
        } else {
            validator.property("date").key("payment.manualDate").pastOrToday();
        }

        validator.property("fromOwner").required();
        validator.property("toOwner").required();
        addAmountValidator(validator, tt);

        validator.property("transferType").key("transfer.type").required();
        validator.property("description").maxLength(1000);
        validator.property("traceNumber").add(new TraceNumberValidation());

        if (transfer.getTransferType() != null) {
            // Custom fields
            validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() {
                @Override
                public Validator getValidator() {
                    return paymentCustomFieldService.getValueValidator(transfer.getTransferType());
                }
            }));
        }
        return validator;
    }

    private Payment insert(final TransferDTO dto, final boolean newTransaction, final boolean simulation) {
        // Verify the parameters
        verify(dto);
        return doInsert(dto, newTransaction, simulation);
    }

    private Transfer insertChargeback(final Transfer transfer, final boolean newTransaction) {
        return transactionHelper.maybeRunInNewTransaction(new Transactional<Transfer>() {
            @Override
            public Transfer afterCommit(final Transfer result) {
                // Ensure the transfer is attached to the current transaction
                return fetchService.fetch(result);
            }

            @Override
            public Transfer doInTransaction(final TransactionStatus status) {
                return performChargeback(transfer);
            }
        }, newTransaction);
    }

    private void insertFees(final LockHandler lockHandler, final Transfer transfer, final boolean forced,
            final BigDecimal originalAmount, final boolean simulation, final Set<ChargedFee> chargedFees) {
        final TransferType transferType = transfer.getType();
        final Account from = transfer.getFrom();
        final Account to = transfer.getTo();
        final TransactionFeeQuery query = new TransactionFeeQuery();
        query.setTransferType(transferType);
        final List<? extends TransactionFee> fees = transactionFeeService.search(query);
        BigDecimal totalPercentage = BigDecimal.ZERO;
        BigDecimal feeTotalAmount = BigDecimal.ZERO;
        Transfer topMost = getTopMost(transfer);
        final Calendar date = topMost.getDate();
        transfer.setChildren(new ArrayList<Transfer>());
        for (final TransactionFee fee : fees) {
            final Account fromAccount = fetchService.fetch(from, Account.Relationships.TYPE,
                    MemberAccount.Relationships.MEMBER);
            final Account toAccount = fetchService.fetch(to, Account.Relationships.TYPE,
                    MemberAccount.Relationships.MEMBER);

            final ChargedFee key = new ChargedFee(fee, fromAccount, toAccount);
            if (chargedFees.contains(key)) {
                throw new ValidationException("payment.error.circularFees");
            }
            chargedFees.add(key);

            // Build the fee transfer
            final BuildTransferWithFeesDTO params = new BuildTransferWithFeesDTO(date, fromAccount, toAccount,
                    originalAmount, fee, false);
            // rate stuff; buildTransfer MUST have these set.
            params.setEmissionDate(transfer.getEmissionDate());
            params.setExpirationDate(transfer.getExpirationDate());
            final Transfer feeTransfer = transactionFeeService.buildTransfer(params);

            // If the fee transfer is null, the fee should not be applied
            if (feeTransfer == null) {
                continue;
            }
            // Ensure the last fee when 100% will be the exact amount left
            if (fee instanceof SimpleTransactionFee && fee.getAmount().isPercentage()) {
                final BigDecimal feeValue = fee.getAmount().getValue();
                // Only when it's not a single fee
                if (!(totalPercentage.equals(BigDecimal.ZERO) && feeValue.doubleValue() == 100.0)) {
                    totalPercentage = totalPercentage.add(feeValue);
                    // TODO: shouldn't this be >= 0 in stead of == 0 (Rinke) ?
                    if (totalPercentage.compareTo(new BigDecimal(100)) == 0 && feeTransfer != null) {
                        feeTransfer.setAmount(originalAmount.subtract(feeTotalAmount));
                    }
                }
            }

            // Insert the fee transfer
            if (feeTransfer != null && feeTransfer.getAmount().floatValue() > PRECISION_DELTA) {
                feeTotalAmount = feeTotalAmount.add(feeTransfer.getAmount());
                feeTransfer.setParent(transfer);
                feeTransfer.setDate(transfer.getDate());
                feeTransfer.setStatus(transfer.getStatus());
                feeTransfer.setNextAuthorizationLevel(transfer.getNextAuthorizationLevel());
                feeTransfer.setProcessDate(transfer.getProcessDate());
                feeTransfer.setExternalTransfer(transfer.getExternalTransfer());
                feeTransfer.setBy(transfer.getBy());

                // Copy custom values of common custom fields from the parent to the fee transfer
                final List<PaymentCustomField> customFields = paymentCustomFieldService.list(feeTransfer.getType(),
                        false);
                if (!CollectionUtils.isEmpty(transfer.getCustomValues())) {
                    final Collection<PaymentCustomFieldValue> feeTransferCustomValues = new ArrayList<PaymentCustomFieldValue>();
                    for (final PaymentCustomFieldValue fieldValue : transfer.getCustomValues()) {
                        final CustomField field = fieldValue.getField();
                        if (customFields.contains(field)) {
                            final PaymentCustomFieldValue newFieldValue = new PaymentCustomFieldValue();
                            newFieldValue.setField(field);
                            newFieldValue.setValue(fieldValue.getValue());
                            feeTransferCustomValues.add(newFieldValue);
                        }
                    }
                    feeTransfer.setCustomValues(feeTransferCustomValues);
                }

                insertTransferAndPayFees(lockHandler, feeTransfer, forced, simulation, chargedFees);
                transfer.getChildren().add(feeTransfer);
            }
        }
    }

    /**
     * Inserts a TN for a transfer with the specified trace number, for the current service client
     * @return true if the TN was inserted
     */
    private boolean insertTN(final Long clientId, final String traceNumber) {
        return transactionHelper.runInNewTransaction(new TransactionCallback<Boolean>() {
            @Override
            public Boolean doInTransaction(final TransactionStatus status) {
                final TraceNumber tn = new TraceNumber();
                tn.setDate(Calendar.getInstance());
                tn.setClientId(clientId);
                tn.setTraceNumber(traceNumber);
                try {
                    traceNumberDao.insert(tn);
                    return true;
                } catch (DaoException e) {
                    status.setRollbackOnly();
                    if (ExceptionUtils.indexOfThrowable(e, DataIntegrityViolationException.class) != -1) {
                        // the unique constraint was violated - It means the trace number was already stored by a payment or by other reverse.
                        // If it was inserted by a payment then we must reverse it.
                        // If was inserted by other reverse then just ignore it.
                        return false;
                    } else {
                        throw e;
                    }
                }
            }
        });
    }

    /**
     * Insert a transfer and it's generated fees
     * @param simulation
     */
    private Transfer insertTransferAndPayFees(final LockHandler lockHandler, Transfer transfer,
            final boolean forced, final boolean simulation, final Set<ChargedFee> chargedFees) {
        final TransferType transferType = transfer.getType();
        final Collection<PaymentCustomFieldValue> customValues = transfer.getCustomValues();

        final Account fromAccount = transfer.getFrom();
        final Account toAccount = transfer.getTo();
        if (fromAccount.equals(toAccount)) {
            throw new ValidationException("payment.error.sameFromAntToInFee");
        }
        if (applicationService.getLockedAccountsOnPayments() == LockedAccountsOnPayments.ALL) {
            lockHandler.lock(fromAccount, toAccount);
        }
        final AccountOwner from = fromAccount.getOwner();
        final AccountOwner to = toAccount.getOwner();

        // Preview fees to determine the deducted amount
        final BigDecimal originalAmount = transfer.getAmount();
        final TransactionFeePreviewDTO preview = transactionFeeService.preview(from, to, transferType,
                transfer.getAmount());
        transfer.setAmount(preview.getFinalAmount());

        final Collection<TransferListener> listeners = getTransferListeners(transfer);

        // validate parent amount
        if (!forced) {
            // Notify any registered listener before validating the amount
            if (!simulation) {
                for (final TransferListener listener : listeners) {
                    listener.onBeforeValidateBalance(transfer);
                }
            }

            validateAmount(transfer.getAmount(), fromAccount, toAccount, transfer);
        }
        transfer.setCustomValues(null);

        // apply rates, but NOT on inserting authorized payments
        RatesToSave rates = new RatesToSave();
        if (transfer.getProcessDate() != null) {
            rates = rateService.applyTransfer(transfer);
            transfer.setEmissionDate(rates.getEmissionDate());
            transfer.setExpirationDate(rates.getExpirationDate());
            transfer.setiRate(rates.getiRate());
        }

        // insert transfer
        transfer = transferDao.insert(transfer);

        // now we have the tranfers' id, we can persist rate info:
        rateService.persist(rates);

        final TransactionNumber transactionNumber = settingsService.getLocalSettings().getTransactionNumber();
        if (transactionNumber != null && transactionNumber.isValid()) {
            final String generated = transactionNumber.generate(transfer.getId(), transfer.getDate());
            transferDao.updateTransactionNumber(transfer.getId(), generated);
        }

        transfer.setCustomValues(customValues);
        paymentCustomFieldService.saveValues(transfer);

        if (transfer.getProcessDate() == null) {
            // Reserve the amount if pending authorization
            accountService.reservePending(transfer);
        } else {
            // Make sure no closed balances exist after the payment. This works both for payments in past and to fix eventual future closed balances
            accountService.removeClosedBalancesAfter(transfer.getFrom(), transfer.getProcessDate());
            accountService.removeClosedBalancesAfter(transfer.getTo(), transfer.getProcessDate());
        }

        // Notify any registered listener after inserting the transfer
        if (!simulation) {
            for (TransferListener listener : listeners) {
                listener.onTransferInserted(transfer);
            }
        }

        // Log this transfer if the transaction succeeds
        final Transfer toLog = transfer;
        CurrentTransactionData.addTransactionCommitListener(new TransactionCommitListener() {
            @Override
            public void onTransactionCommit() {
                loggingHandler.logTransfer(toLog);
                // Notify the registered listeners if the transfer is processed
                if (!simulation && toLog.getProcessDate() != null && !listeners.isEmpty()) {
                    transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() {
                        @Override
                        protected void doInTransactionWithoutResult(final TransactionStatus status) {
                            Transfer fetchedTransfer = fetchService.fetch(toLog, Payment.Relationships.FROM,
                                    Payment.Relationships.TO);
                            for (TransferListener listener : listeners) {
                                try {
                                    listener.onTransferProcessed(fetchedTransfer);
                                } catch (Exception e) {
                                    LOG.warn("Error running TransferListener " + listener, e);
                                }
                            }
                        }
                    });
                }
            }
        });

        insertFees(lockHandler, transfer, forced, originalAmount, simulation, chargedFees);
        return transfer;
    }

    private Transfer performChargeback(final LockHandler lockHandler, Transfer transfer,
            final Transfer parentChargeback) {

        transfer = fetchService.fetch(transfer, Transfer.Relationships.CHILDREN);

        if (applicationService.getLockedAccountsOnPayments() == LockedAccountsOnPayments.ALL) {
            lockHandler.lock(transfer.getFrom(), transfer.getTo());
        }

        // Validate the amount
        validateAmount(transfer.getAmount(), transfer.getTo(), transfer.getFrom(), transfer);

        // Duplicate the transfer, setting relevant properties on the charge-back
        Transfer chargeback = transferDao.duplicate(transfer);
        chargeback.setTraceNumber(null);
        ServiceClient serviceClient = LoggedUser.serviceClient();
        if (serviceClient != null) {
            chargeback.setClientId(serviceClient.getId());
        }
        chargeback.setChargebackOf(transfer);
        chargeback.setParent(parentChargeback);
        chargeback.setAmount(chargeback.getAmount().negate());
        final Calendar now = Calendar.getInstance();
        chargeback.setDate(now);
        chargeback.setProcessDate(now);
        chargeback.setStatus(Payment.Status.PROCESSED);
        if (LoggedUser.hasUser()) {
            chargeback.setBy(LoggedUser.element());
        }
        chargeback.setReceiver(null);
        chargeback.setScheduledPayment(null);

        // Build the description according to settings
        final LocalSettings localSettings = settingsService.getLocalSettings();
        final Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("description", transfer.getDescription());
        variables.put("date", localSettings.getDateConverter().toString(transfer.getDate()));
        chargeback.setDescription(
                MessageProcessingHelper.processVariables(localSettings.getChargebackDescription(), variables));

        // Insert the chargeback
        chargeback = transferDao.insert(chargeback, false);

        // Copy the custom values from the original transfer
        if (CollectionUtils.isNotEmpty(transfer.getCustomValues())) {
            final Collection<PaymentCustomFieldValue> customValues = new ArrayList<PaymentCustomFieldValue>();
            if (transfer.getCustomValues() != null) {
                for (final PaymentCustomFieldValue original : transfer.getCustomValues()) {
                    final PaymentCustomFieldValue newValue = new PaymentCustomFieldValue();
                    newValue.setTransfer(chargeback);
                    newValue.setField(original.getField());
                    newValue.setStringValue(original.getStringValue());
                    newValue.setPossibleValue(original.getPossibleValue());
                    customValues.add(newValue);
                }
            }
            chargeback.setCustomValues(customValues);
            paymentCustomFieldService.saveValues(chargeback);
        }

        // Update the original transfer
        transfer = transferDao.updateChargeBack(transfer, chargeback);

        // Assign the transaction number
        final TransactionNumber transactionNumber = settingsService.getLocalSettings().getTransactionNumber();
        if (transactionNumber != null && transactionNumber.isValid()) {
            final String generated = transactionNumber.generate(chargeback.getId(), chargeback.getDate());
            transferDao.updateTransactionNumber(chargeback.getId(), generated);
        }

        // Make sure no closed balances exist on the future, to fix eventual future closed balances
        accountService.removeClosedBalancesAfter(chargeback.getFrom(), chargeback.getProcessDate());
        accountService.removeClosedBalancesAfter(chargeback.getTo(), chargeback.getProcessDate());

        // Correct the rates, if available
        rateService.chargeback(transfer, chargeback);

        // Insert children chargebacks
        for (final Transfer child : transfer.getChildren()) {
            performChargeback(lockHandler, child, chargeback);
        }

        return chargeback;
    }

    private Transfer performChargeback(final Transfer transfer) {
        LockHandler lockHandler = lockHandlerFactory.getLockHandlerIfLockingAccounts();
        try {
            // If only the source account needs to be locked, lock it here
            if (applicationService.getLockedAccountsOnPayments() == LockedAccountsOnPayments.ORIGIN
                    && transfer.getTo().getCreditLimit() != null) {
                lockHandler.lock(transfer.getTo());
            }
            return performChargeback(lockHandler, transfer, null);
        } finally {
            if (lockHandler != null) {
                lockHandler.release();
            }
        }
    }

    /**
     * Locks the accounts which need to be locked, and perform the insert
     */
    private Payment performInsert(final LockHandler lockHandler, final TransferDTO dto, final boolean simulation) {
        final TransferType transferType = dto.getTransferType();
        final Account fromAccount = fetchService.fetch(dto.getFrom(),
                RelationshipHelper.nested(Account.Relationships.TYPE, AccountType.Relationships.CURRENCY));
        final Account toAccount = fetchService.fetch(dto.getTo(), MemberAccount.Relationships.MEMBER);
        if (applicationService.getLockedAccountsOnPayments() == LockedAccountsOnPayments.ALL) {
            lockHandler.lock(fromAccount, toAccount);
        }

        // Get the feedback deadline
        Calendar feedbackDeadline = null;
        if (transferType.isRequiresFeedback()) {
            feedbackDeadline = transferType.getFeedbackExpirationTime().add(Calendar.getInstance());
        }
        boolean hasMaxAmountPerDay = BigDecimalHelper.nvl(transferType.getMaxAmountPerDay())
                .compareTo(BigDecimal.ZERO) > 0;

        Payment payment;
        // Check scheduling
        final Calendar now = Calendar.getInstance();
        if (CollectionUtils.isEmpty(dto.getPayments())) {
            // Not scheduled - build a transfer
            final String traceNumber = dto.getTraceNumber();
            final Long clientId = dto.getClientId();

            Transfer transfer = new Transfer();
            transfer.setFrom(fromAccount);
            transfer.setTo(toAccount);
            transfer.setBy(dto.getBy());
            transfer.setDate(now);
            transfer.setAmount(dto.getAmount());
            transfer.setType(transferType);
            transfer.setDescription(dto.getDescription());
            transfer.setAccountFeeLog(dto.getAccountFeeLog());
            transfer.setLoanPayment(dto.getLoanPayment());
            transfer.setParent(dto.getParent());
            transfer.setReceiver(dto.getReceiver());
            transfer.setExternalTransfer(dto.getExternalTransfer());
            transfer.setCustomValues(dto.getCustomValues());
            transfer.setTraceNumber(traceNumber);
            transfer.setClientId(clientId);
            transfer.setTraceData(dto.getTraceData());
            transfer.setTransactionFeedbackDeadline(feedbackDeadline);
            if (transferType.isLoanType()) {
                transfer.setEmissionDate(dto.getEmissionDate());
                transfer.setExpirationDate(dto.getExpirationDate());
                transfer.setiRate(dto.getiRate());
            }
            // Lock the accounts
            if (applicationService.getLockedAccountsOnPayments() == LockedAccountsOnPayments.ORIGIN
                    && (fromAccount.getCreditLimit() != null || hasMaxAmountPerDay)) {
                lockHandler.lock(fromAccount);
            }

            // Tests whether there is a valid ticket to be used
            final Ticket ticket = fetchService.reload(dto.getTicket());
            if (ticket != null) {
                if (ticket.getStatus() != Ticket.Status.PENDING) {
                    throw new EntityNotFoundException(Ticket.class);
                }
                // Force the ticket parameters on the payment
                if (ticket.getAmount() != null && !ticket.getAmount().equals(transfer.getAmount())) {
                    // TODO add a translation key
                    throw new ValidationException(
                            "The payment amount is not the expected one according to the ticket");
                }
                if (!ticket.getTo().equals(transfer.getToOwner())) {
                    // TODO add a translation key
                    throw new ValidationException(
                            "The payment destination member is not the expected one according to the ticket");
                }
                if (StringUtils.isNotEmpty(ticket.getDescription())
                        && StringUtils.isEmpty(transfer.getDescription())) {
                    transfer.setDescription(ticket.getDescription());
                }
            }

            // Determine whether the transfer is authorized
            AuthorizationLevel firstAuthorizationLevel;
            final Transfer parent = fetchService.fetch(dto.getParent(),
                    Transfer.Relationships.NEXT_AUTHORIZATION_LEVEL);
            if (parent != null && parent.getNextAuthorizationLevel() != null) {
                firstAuthorizationLevel = parent.getNextAuthorizationLevel();
            } else {
                firstAuthorizationLevel = firstAuthorizationLevel(transferType, transfer.getAmount(),
                        transfer.getFromOwner());
            }

            // Authorized payments are not allowed in past date
            if (firstAuthorizationLevel != null) {
                if (dto.getDate() != null && !DateUtils.isSameDay(dto.getDate(), Calendar.getInstance())) {
                    throw new AuthorizedPaymentInPastException();
                }
            }

            // Set the status according to the authorization level
            if (firstAuthorizationLevel == null) {
                transfer.setProcessDate(dto.getDate() == null ? now : dto.getDate());
                transfer.setStatus(Transfer.Status.PROCESSED);
            } else {
                transfer.setStatus(Transfer.Status.PENDING);
                transfer.setNextAuthorizationLevel(firstAuthorizationLevel);
            }

            // Within the critical session, we must check the trace number again, as another thread could have inserted a reverse on it
            if (clientId != null && StringUtils.isNotEmpty(traceNumber)) {
                // If the TN was not inserted then this payment was already reversed, and should fail
                if (!insertTN(clientId, traceNumber)) {
                    throw new ValidationException("traceNumber", "transfer.traceNumber",
                            new UniqueError(traceNumber));
                }
            }

            // Validate the max amount today
            if (!dto.isForced()) {
                validateMaxAmountAtDate(null, fromAccount, transferType, null, transfer.getAmount());
            }

            // Insert the transfer and pay fees
            transfer = insertTransferAndPayFees(lockHandler, transfer, dto.isForced(), simulation,
                    new HashSet<ChargedFee>());

            // Process the authorization automatically when the authorizer is performing a payment as member
            payment = transferAuthorizationService.authorizeOnInsert(lockHandler, transfer);

            // Complete the ticket, if it exists
            if (ticket != null) {
                ticket.setAmount(payment.getAmount());
                ticket.setDescription(payment.getDescription());
                if (payment.getFrom().getOwner() instanceof Member) {
                    ticket.setFrom((Member) payment.getFrom().getOwner());
                } else {
                    ticket.setFrom(null);
                }
                ticket.setTo((Member) payment.getTo().getOwner());
                ticket.setStatus(Ticket.Status.OK);
                ticket.setTransfer((Transfer) payment);
            }

        } else {
            // Scheduled payment

            final boolean reserveTotalAmount = transferType.isReserveTotalAmountOnScheduling();
            if (!dto.isForced() && (reserveTotalAmount || hasMaxAmountPerDay)) {
                // Ensure the from account is locked, to prevent concurrent access to available balance (which could allow an account pass the limit)
                lockHandler.lock(fromAccount);

                // Validate the account has balance for the total amount
                if (reserveTotalAmount) {
                    validateAmount(dto.getAmount(), fromAccount, null, null);
                }

                // Validate the max amount today
                if (hasMaxAmountPerDay) {
                    for (final ScheduledPaymentDTO current : dto.getPayments()) {
                        validateMaxAmountAtDate(current.getDate(), fromAccount, transferType, null,
                                current.getAmount());
                    }
                    // TODO now we're controlling the total amount, but should control by installment
                }
            }

            final Collection<PaymentCustomFieldValue> customValues = dto.getCustomValues();
            ScheduledPayment scheduledPayment = new ScheduledPayment();
            scheduledPayment.setFrom(fromAccount);
            scheduledPayment.setTo(toAccount);
            scheduledPayment.setBy(dto.getBy());
            scheduledPayment.setDate(now);
            scheduledPayment.setAmount(dto.getAmount());
            scheduledPayment.setType(transferType);
            scheduledPayment.setDescription(dto.getDescription());
            scheduledPayment.setStatus(Payment.Status.SCHEDULED);
            scheduledPayment.setReserveAmount(reserveTotalAmount);
            scheduledPayment.setShowToReceiver(
                    transferType.isShowScheduledPaymentsToDestination() || dto.isShowScheduledToReceiver());
            scheduledPayment.setTransactionFeedbackDeadline(feedbackDeadline);

            scheduledPayment = scheduledPaymentDao.insert(scheduledPayment);
            scheduledPayment.setCustomValues(new ArrayList<PaymentCustomFieldValue>(customValues));
            paymentCustomFieldService.saveValues(scheduledPayment);

            final List<Transfer> scheduledTransfers = new ArrayList<Transfer>();
            Transfer transferToProcess = null;
            for (final ScheduledPaymentDTO current : dto.getPayments()) {
                final TransferDTO currentDTO = (TransferDTO) dto.clone();
                currentDTO.setDate(current.getDate());
                currentDTO.setAmount(current.getAmount());
                currentDTO.setScheduledPayment(scheduledPayment);
                Transfer transfer = new Transfer();
                transfer.setFrom(fromAccount);
                transfer.setTo(dto.getTo());
                transfer.setBy(dto.getBy());
                transfer.setDate(current.getDate());
                transfer.setAmount(current.getAmount());
                transfer.setType(transferType);
                transfer.setDescription(dto.getDescription());
                transfer.setStatus(Transfer.Status.SCHEDULED);
                transfer.setScheduledPayment(scheduledPayment);
                // When the payment is scheduled for today, process it now
                if (DateUtils.isSameDay(now, transfer.getDate())) {
                    transferToProcess = transfer;
                    transfer.setDate(now);
                }
                transfer = transferDao.insert(transfer);
                transfer.setCustomValues(new ArrayList<PaymentCustomFieldValue>());
                if (customValues != null) {
                    for (final PaymentCustomFieldValue fieldValue : customValues) {
                        final PaymentCustomFieldValue newValue = new PaymentCustomFieldValue();
                        newValue.setField(fieldValue.getField());
                        newValue.setStringValue(fieldValue.getStringValue());
                        newValue.setPossibleValue(fieldValue.getPossibleValue());
                        transfer.getCustomValues().add(newValue);
                    }
                }
                paymentCustomFieldService.saveValues(transfer);
                scheduledTransfers.add(transfer);
            }
            scheduledPayment.setTransfers(scheduledTransfers);

            // When the scheduled payment is set to reserve the amount, add the corresponding amount reservation
            if (scheduledPayment.isReserveAmount()) {
                accountService.reserve(scheduledPayment);
            }

            // When the first transfer should already by processed, do it now
            if (transferToProcess != null) {
                doProcessScheduledTransfer(lockHandler, transferToProcess, true, false, true);
            }
            payment = scheduledPayment;
        }

        // Return the transfer object
        return payment;
    }

    /**
     * Locks the accounts which need to be locked, and perform the insert
     */
    private Payment performInsert(final TransferDTO dto, final boolean simulation) {
        LockHandler lockHandler = lockHandlerFactory.getLockHandlerIfLockingAccounts();
        try {
            return performInsert(lockHandler, dto, simulation);
        } finally {
            if (lockHandler != null) {
                lockHandler.release();
            }
        }
    }

    private Transfer processScheduledTransfer(final Transfer transfer, final boolean failOnError,
            final boolean notifyPayer, final boolean notifyReceiver) {
        return transactionHelper.maybeRunInNewTransaction(new Transactional<Transfer>() {
            @Override
            public Transfer afterCommit(final Transfer result) {
                // Ensure the transfer is attached to the current transaction
                return fetchService.fetch(result);
            }

            @Override
            public Transfer doInTransaction(final TransactionStatus status) {
                return doProcessScheduledTransfer(transfer, failOnError, notifyPayer, notifyReceiver);
            };
        });
    }

    private ScheduledPayment updateScheduledPaymentStatus(ScheduledPayment scheduledPayment) {
        scheduledPayment = fetchService.fetch(scheduledPayment, ScheduledPayment.Relationships.TRANSFERS);
        scheduledPayment.setStatus(Payment.Status.PROCESSED);
        for (final Transfer transfer : scheduledPayment.getTransfers()) {
            if (transfer.getProcessDate() == null) {
                scheduledPayment.setStatus(transfer.getStatus());
                break;
            }
        }
        return scheduledPaymentDao.update(scheduledPayment);
    }

    private void validate(final TransferDTO params) {
        getTransferValidator(params).validate(params);
    }

    /**
     * Validates the given amount
     */
    private void validateAmount(final BigDecimal amount, Account fromAccount, final Account toAccount,
            final Transfer transfer) {
        // Validate the from account credit limit ...
        final LocalSettings localSettings = settingsService.getLocalSettings();
        if (fromAccount != null) {
            final BigDecimal creditLimit = fromAccount.getCreditLimit();
            if (creditLimit != null) {
                // ... only if not unlimited
                final AccountStatus fromStatus = accountService.getCurrentStatus(new AccountDTO(fromAccount));
                if (creditLimit.abs().floatValue() > -PRECISION_DELTA) {
                    final BigDecimal available = localSettings.round(fromStatus.getAvailableBalance());
                    if (available.subtract(amount).floatValue() < -PRECISION_DELTA) {
                        final boolean isOriginalAccount = transfer == null ? true
                                : fromAccount.equals(transfer.getRootTransfer().getFrom());
                        fromAccount = fetchService.fetch(fromAccount, Account.Relationships.TYPE);
                        throw new NotEnoughCreditsException(fromAccount, amount, isOriginalAccount);
                    }
                }
            }
        }

        // Validate the to account upper credit limit
        if (toAccount != null) {
            final BigDecimal upperCreditLimit = toAccount.getUpperCreditLimit();
            if (upperCreditLimit != null && upperCreditLimit.floatValue() > PRECISION_DELTA) {
                final BigDecimal balance = accountService.getBalance(new AccountDateDTO(toAccount));
                if (upperCreditLimit.subtract(balance).subtract(amount).floatValue() < -PRECISION_DELTA) {
                    throw new UpperCreditLimitReachedException(
                            localSettings.getUnitsConverter(toAccount.getType().getCurrency().getPattern())
                                    .toString(toAccount.getUpperCreditLimit()),
                            toAccount, amount);
                }
            }
        }
    }

    /**
     * Validates if a given transfer type is valid
     */
    private TransferType validateTransferType(final TransferDTO params) {
        final TransferType transferType = transferTypeService.load(params.getTransferType().getId(),
                TransferType.Relationships.FROM, TransferType.Relationships.TO);
        final TransferTypeQuery ttQuery = new TransferTypeQuery();
        ttQuery.setChannel(params.getChannel());
        if (params.isAutomatic()) {
            ttQuery.setContext(
                    transferType.isLoanType() ? TransactionContext.AUTOMATIC_LOAN : TransactionContext.AUTOMATIC);
        } else {
            ttQuery.setContext(params.getContext());
        }
        final TransactionContext context = ttQuery.getContext();
        if (context != TransactionContext.AUTOMATIC && context != TransactionContext.AUTOMATIC_LOAN) {
            ttQuery.setUsePriority(true);
        }
        ttQuery.setCurrency(params.getCurrency());
        ttQuery.setFromAccountType(transferType.getFrom());
        ttQuery.setToAccountType(transferType.getTo());
        final AccountOwner fromOwner = params.getFromOwner();

        // For non-automatic payments, ensure there is permission for the TransferType
        if (context != TransactionContext.AUTOMATIC && context != TransactionContext.AUTOMATIC_LOAN) {
            if (params.getBy() != null && fromOwner != null
                    && !params.getBy().getAccountOwner().equals(fromOwner)) {
                // Set by when performing a payment in behalf of someone
                ttQuery.setBy(params.getBy());
            } else {
                // Test the permission for the payment
                if (fromOwner instanceof Member) {
                    ttQuery.setGroup(((Member) fromOwner).getGroup());
                } else if (LoggedUser.hasUser()) {
                    ttQuery.setGroup(LoggedUser.group());
                }
            }
        }
        ttQuery.setFromOwner(fromOwner);
        ttQuery.setToOwner(params.getToOwner());
        final List<TransferType> possibleTypes = transferTypeService.search(ttQuery);
        if (possibleTypes == null || !possibleTypes.contains(transferType)) {
            throw new UnexpectedEntityException("Transfer type not found for query");
        }
        return transferType;
    }

    private TransferDTO verify(final DoPaymentDTO params) {
        // Build and verify the DTO
        final TransferDTO dto = new TransferDTO();
        dto.setAmount(params.getAmount());
        dto.setCurrency(params.getCurrency());
        dto.setChannel(params.getChannel());
        dto.setContext(params.getContext());
        if (params.getDate() != null) {
            dto.setDate(params.getDate());
        }
        dto.setDescription(params.getDescription());
        dto.setFromOwner(params.getFrom() == null ? LoggedUser.accountOwner() : params.getFrom());

        if (LoggedUser.hasUser() && !LoggedUser.isWebService()) {
            dto.setBy(LoggedUser.element());
        }

        dto.setToOwner(params.getTo());
        dto.setTicket(params.getTicket());
        dto.setTransferType(params.getTransferType());
        dto.setReceiver(params.getReceiver());
        dto.setPayments(params.getPayments());
        dto.setCustomValues(params.getCustomValues());
        dto.setTraceData(params.getTraceData());
        dto.setShowScheduledToReceiver(params.isShowScheduledToReceiver());

        ServiceClient serviceClient = LoggedUser.serviceClient();
        if (serviceClient != null && params.getTraceNumber() != null) {
            dto.setTraceNumber(params.getTraceNumber());
            dto.setClientId(serviceClient.getId());
        }

        verify(dto);

        return dto;
    }

    private void verify(final TransferDTO params) {

        if (params.getFrom() != null) {
            final Account from = fetchService.fetch(params.getFrom(), MemberAccount.Relationships.MEMBER);
            params.setFromOwner(from.getOwner());
        }
        if (params.getTo() != null) {
            final Account to = fetchService.fetch(params.getTo(), MemberAccount.Relationships.MEMBER);
            params.setToOwner(to.getOwner());
        }

        validate(params);

        final AccountOwner fromOwner = params.getFromOwner();
        final AccountOwner toOwner = params.getToOwner();

        // Validate the transfer type
        final TransferType transferType = validateTransferType(params);

        // Retrieve the from and to accounts
        final Account fromAccount = accountService.getAccount(new AccountDTO(fromOwner, transferType.getFrom()));
        final Account toAccount = accountService.getAccount(new AccountDTO(toOwner, transferType.getTo()));

        if (fromAccount.equals(toAccount)) {
            throw new ValidationException(new ValidationError("payment.error.sameAccount"));
        }

        // Retrieve the amount
        final BigDecimal amount = params.getAmount();

        // Check the minimum payment
        if (amount.compareTo(getMinimumPayment()) == -1) {
            final LocalSettings localSettings = settingsService.getLocalSettings();

            throw new TransferMinimumPaymentException(
                    localSettings.getUnitsConverter(fromAccount.getType().getCurrency().getPattern())
                            .toString(getMinimumPayment()),
                    fromAccount, amount);
        }

        // Update some retrieved parameters on the DTO
        params.setTransferType(transferType);
        params.setFrom(fromAccount);
        params.setTo(toAccount);
        if (StringUtils.isBlank(params.getDescription())) {
            params.setDescription(transferType.getDescription());
        }
    }

}