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

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.services.transactions.LoanServiceImpl.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.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import nl.strohalm.cyclos.access.AdminMemberPermission;
import nl.strohalm.cyclos.access.AdminSystemPermission;
import nl.strohalm.cyclos.dao.accounts.loans.LoanDAO;
import nl.strohalm.cyclos.dao.accounts.loans.LoanPaymentDAO;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.accounts.Currency;
import nl.strohalm.cyclos.entities.accounts.MemberAccount;
import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner;
import nl.strohalm.cyclos.entities.accounts.external.ExternalTransfer;
import nl.strohalm.cyclos.entities.accounts.loans.Loan;
import nl.strohalm.cyclos.entities.accounts.loans.LoanParameters;
import nl.strohalm.cyclos.entities.accounts.loans.LoanPayment;
import nl.strohalm.cyclos.entities.accounts.loans.LoanPayment.Status;
import nl.strohalm.cyclos.entities.accounts.loans.LoanPaymentQuery;
import nl.strohalm.cyclos.entities.accounts.loans.LoanQuery;
import nl.strohalm.cyclos.entities.accounts.loans.LoanRepaymentAmountsDTO;
import nl.strohalm.cyclos.entities.accounts.transactions.Payment;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferAuthorizationDTO;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferQuery;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferType;
import nl.strohalm.cyclos.entities.alerts.MemberAlert;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.groups.AdminGroup;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.services.InitializingService;
import nl.strohalm.cyclos.services.accounts.AccountDTO;
import nl.strohalm.cyclos.services.accounts.AccountServiceLocal;
import nl.strohalm.cyclos.services.accounts.rates.RateServiceLocal;
import nl.strohalm.cyclos.services.alerts.AlertServiceLocal;
import nl.strohalm.cyclos.services.customization.PaymentCustomFieldServiceLocal;
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.transactions.exceptions.AuthorizedPaymentInPastException;
import nl.strohalm.cyclos.utils.Amount;
import nl.strohalm.cyclos.utils.CacheCleaner;
import nl.strohalm.cyclos.utils.DateHelper;
import nl.strohalm.cyclos.utils.Period;
import nl.strohalm.cyclos.utils.RelationshipHelper;
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.CoercionHelper;
import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler;
import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType;
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.PositiveNonZeroValidation;
import nl.strohalm.cyclos.utils.validation.PropertyValidation;
import nl.strohalm.cyclos.utils.validation.RequiredError;
import nl.strohalm.cyclos.utils.validation.RequiredValidation;
import nl.strohalm.cyclos.utils.validation.ValidationError;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import nl.strohalm.cyclos.utils.validation.Validator;

import org.apache.commons.lang.mutable.MutableBoolean;
import org.apache.commons.lang.time.DateUtils;
import org.springframework.transaction.TransactionStatus;

/**
 * Implementation for loan service
 * @author luis
 */
public class LoanServiceImpl implements LoanServiceLocal, InitializingService {

    /**
     * Validates a transfer type, forcing it to be a loan type
     * @author luis
     */
    private class LoanTypeValidation implements PropertyValidation {

        private static final long serialVersionUID = -7166494808222423923L;

        public LoanTypeValidation() {
        }

        @Override
        public ValidationError validate(final Object object, final Object name, final Object value) {
            final TransferType transferType = (TransferType) value;
            if (transferType == null) {
                return null;
            }
            // Ensure transfer type is a loan
            if (!transferType.isLoanType()) {
                return new InvalidError();
            }
            return null;
        }
    }

    /**
     * Validator for multi-payment loan's collection of loan payment
     * @author luis
     */
    private final class MultiPaymentValidation implements PropertyValidation {
        private static final long serialVersionUID = -7905875152926109032L;

        @Override
        @SuppressWarnings("unchecked")
        public ValidationError validate(final Object object, final Object name, final Object value) {
            final GrantMultiPaymentLoanDTO dto = (GrantMultiPaymentLoanDTO) object;
            final Collection<LoanPayment> payments = (Collection<LoanPayment>) value;
            if (payments == null || payments.isEmpty()) {
                return null;
            }
            Calendar lastExpiration = DateHelper.truncate(Calendar.getInstance());
            BigDecimal paymentAmounts = BigDecimal.ZERO;
            final BigDecimal totalAmount = dto.getAmount();
            final boolean processAmount = totalAmount != null && totalAmount.compareTo(BigDecimal.ZERO) == 1;
            // Validate each payment
            for (final LoanPayment payment : payments) {
                // Check for required expiration date
                ValidationError error = RequiredValidation.instance().validate(object, name,
                        payment.getExpirationDate());
                // Check for required amount
                final BigDecimal paymentAmount = payment.getAmount();
                if (error == null) {
                    error = RequiredValidation.instance().validate(object, name, paymentAmount);
                }
                if (error == null) {
                    error = PositiveNonZeroValidation.instance().validate(object, name, paymentAmount);
                }
                // Check if the current expiration date is after the last expiration date
                if (error == null && lastExpiration.after(payment.getExpirationDate())) {
                    error = new ValidationError("loan.grant.error.unsortedPayments");
                }
                // Return any error for this payment
                if (error != null) {
                    return error;
                }
                // Sum an accumulator for total amount comparision
                if (processAmount) {
                    paymentAmounts = paymentAmounts.add(paymentAmount);
                }
                lastExpiration = payment.getExpirationDate();
            }
            // Check if the payment amount sum == total amount
            if ((paymentAmounts.subtract(totalAmount)).abs().floatValue() > PRECISION_DELTA) {
                return new ValidationError("loan.grant.error.invalidAmount");
            }
            return null;
        }
    }

    private TransferAuthorizationServiceLocal transferAuthorizationService;
    private static final float PRECISION_DELTA = 0.0001F;
    private AccountServiceLocal accountService;
    private AlertServiceLocal alertService;
    private PaymentCustomFieldServiceLocal paymentCustomFieldService;
    private FetchServiceLocal fetchService;
    private LoanDAO loanDao;
    private LoanPaymentDAO loanPaymentDao;
    private PaymentServiceLocal paymentService;
    private RateServiceLocal rateService;
    private SettingsServiceLocal settingsService;
    private final Map<Loan.Type, LoanHandler> handlersByType = new EnumMap<Loan.Type, LoanHandler>(Loan.Type.class);
    private PermissionServiceLocal permissionService;
    private MemberNotificationHandler memberNotificationHandler;

    private TransactionHelper transactionHelper;

    @Override
    public void alertExpiredLoans(final Calendar time) {
        final Calendar deadline = DateHelper.truncate(time);
        deadline.add(Calendar.DATE, -1);
        final LoanPaymentQuery query = new LoanPaymentQuery();
        query.setResultType(ResultType.ITERATOR);
        query.fetch(RelationshipHelper.nested(LoanPayment.Relationships.LOAN, Loan.Relationships.TRANSFER,
                Payment.Relationships.TO, MemberAccount.Relationships.MEMBER, Element.Relationships.GROUP));
        query.setExpirationPeriod(Period.endingAt(deadline));
        query.setStatus(LoanPayment.Status.OPEN);
        CacheCleaner cacheCleaner = new CacheCleaner(fetchService);
        final List<LoanPayment> payments = search(query);
        for (final LoanPayment payment : payments) {
            final Loan loan = payment.getLoan();
            payment.setStatus(LoanPayment.Status.EXPIRED);
            loanPaymentDao.update(payment);
            // Create an alert
            final Member member = (Member) loan.getTransfer().getTo().getOwner();
            alertService.create(member, MemberAlert.Alerts.EXPIRED_LOAN);
            // Notify the member
            memberNotificationHandler.expiredLoanNotification(payment);
            cacheCleaner.clearCache();
        }
    }

    @Override
    public List<LoanPayment> calculatePaymentProjection(final ProjectionDTO params) {
        params.setTransferType(fetchService.fetch(params.getTransferType()));
        if (params.getDate() == null) {
            params.setDate(Calendar.getInstance());
        }
        getProjectionValidator().validate(params);
        return handlersByType.get(params.getTransferType().getLoan().getType()).calculatePaymentProjection(params);
    }

    @Override
    public LoanPayment discard(final LoanPaymentDTO dto) {
        final LoanPaymentDTO dateDto = new LoanPaymentDTO();
        dateDto.setLoan(dto.getLoan());
        dateDto.setLoanPayment(dto.getLoanPayment());
        return doDiscard(dateDto);
    }

    @Override
    public LoanPayment discardByExternalTransfer(final Loan loan, final ExternalTransfer externalTransfer)
            throws UnexpectedEntityException {
        final LoanPaymentDTO dto = new LoanPaymentDTO();
        dto.setLoan(loan);
        final LoanPayment loanPayment = doDiscard(dto);
        loanPayment.setExternalTransfer(externalTransfer);
        return loanPaymentDao.update(loanPayment);
    }

    @Override
    public LoanRepaymentAmountsDTO getLoanPaymentAmount(final LoanPaymentDTO dto) {
        final LoanRepaymentAmountsDTO ret = new LoanRepaymentAmountsDTO();
        Calendar date = dto.getDate();
        if (date == null) {
            date = Calendar.getInstance();
        }
        final Loan loan = fetchService.fetch(dto.getLoan(), Loan.Relationships.TRANSFER,
                Loan.Relationships.PAYMENTS);
        LoanPayment payment = fetchService.fetch(dto.getLoanPayment());
        if (payment == null) {
            payment = loan.getFirstOpenPayment();
        }
        ret.setLoanPayment(payment);

        // Update the dto with fetched values
        dto.setLoan(loan);
        dto.setLoanPayment(payment);

        if (payment != null) {
            payment = fetchService.fetch(payment, LoanPayment.Relationships.TRANSFERS);
            final BigDecimal paymentAmount = payment.getAmount();
            BigDecimal remainingAmount = paymentAmount;
            Calendar expirationDate = payment.getExpirationDate();
            Calendar lastPaymentDate = (Calendar) expirationDate.clone();
            expirationDate = DateUtils.truncate(expirationDate, Calendar.DATE);
            final LoanParameters parameters = loan.getParameters();
            Collection<Transfer> transfers = payment.getTransfers();
            if (transfers == null) {
                transfers = Collections.emptyList();
            }
            final BigDecimal expirationDailyInterest = CoercionHelper.coerce(BigDecimal.class,
                    parameters.getExpirationDailyInterest());
            final LocalSettings localSettings = settingsService.getLocalSettings();
            final MathContext mathContext = localSettings.getMathContext();
            for (final Transfer transfer : transfers) {
                Calendar trfDate = transfer.getDate();
                trfDate = DateUtils.truncate(trfDate, Calendar.DATE);
                final BigDecimal trfAmount = transfer.getAmount();
                BigDecimal actualAmount = trfAmount;
                final int diffDays = (int) ((trfDate.getTimeInMillis() - expirationDate.getTimeInMillis())
                        / DateUtils.MILLIS_PER_DAY);
                if (diffDays > 0 && expirationDailyInterest != null) {
                    // Apply interest
                    actualAmount = actualAmount.subtract(remainingAmount.multiply(new BigDecimal(diffDays))
                            .multiply(expirationDailyInterest.divide(new BigDecimal(100), mathContext)));
                }
                remainingAmount = remainingAmount.subtract(actualAmount);
                lastPaymentDate = (Calendar) trfDate.clone();
            }
            date = DateHelper.truncate(date);
            BigDecimal remainingAmountAtDate = remainingAmount;
            final int diffDays = (int) ((date.getTimeInMillis()
                    - (expirationDate.before(lastPaymentDate) ? lastPaymentDate.getTimeInMillis()
                            : expirationDate.getTimeInMillis()))
                    / DateUtils.MILLIS_PER_DAY);
            if (diffDays > 0 && expirationDailyInterest != null) {
                // Apply interest
                remainingAmountAtDate = remainingAmountAtDate.add(remainingAmount.multiply(new BigDecimal(diffDays))
                        .multiply(expirationDailyInterest.divide(new BigDecimal(100), mathContext)));
            }
            final Amount expirationFee = parameters.getExpirationFee();
            if (expirationFee != null && (remainingAmountAtDate.compareTo(BigDecimal.ZERO) == 1)
                    && expirationDate.before(date) && (expirationFee.getValue().compareTo(BigDecimal.ZERO) == 1)) {
                // Apply expiration fee
                remainingAmountAtDate = remainingAmountAtDate.add(expirationFee.apply(remainingAmount));
            }
            // Round the result
            ret.setRemainingAmountAtExpirationDate(localSettings.round(remainingAmount));
            ret.setRemainingAmountAtDate(localSettings.round(remainingAmountAtDate));
        }
        return ret;
    }

    @Override
    public TransactionSummaryVO getOpenLoansSummary(final Currency currency) {
        final LoanQuery query = new LoanQuery();
        query.setStatus(Loan.Status.OPEN);
        query.setCurrency(currency);
        if (LoggedUser.hasUser()) {
            AdminGroup adminGroup = LoggedUser.group();
            adminGroup = fetchService.fetch(adminGroup, AdminGroup.Relationships.MANAGES_GROUPS);
            query.setGroups(adminGroup.getManagesGroups());
        }
        return buildSummary(query);
    }

    @Override
    public Loan grant(final GrantLoanDTO params) {
        return doGrant(params, true);
    }

    @Override
    public Loan grantForGuarantee(final GrantLoanDTO params, final boolean automaticAuthorize) {
        final Loan loan = insert(params);

        if (automaticAuthorize && permissionService.hasPermission(AdminSystemPermission.PAYMENTS_AUTHORIZE)
                && (loan.getTransfer().getNextAuthorizationLevel() != null)) {
            final TransferAuthorizationDTO transferAuthorizationDto = new TransferAuthorizationDTO();
            transferAuthorizationDto.setTransfer(loan.getTransfer());
            transferAuthorizationService.authorize(transferAuthorizationDto, false);
        }

        return loan;
    }

    @Override
    public void initializeService() {
        alertExpiredLoans(Calendar.getInstance());
    }

    @Override
    public Loan insert(final GrantLoanDTO params) {
        return doGrant(params, false);
    }

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

    @Override
    public TransactionSummaryVO loanSummary(final Member member) {
        final LoanQuery query = new LoanQuery();
        query.setMember(member);
        query.setStatus(Loan.Status.OPEN);
        return buildSummary(query);
    }

    @Override
    public Loan markAsInProcess(final Loan loan) throws UnexpectedEntityException {
        return markAs(loan, LoanPayment.Status.EXPIRED, LoanPayment.Status.IN_PROCESS);
    }

    @Override
    public Loan markAsRecovered(final Loan loan) throws UnexpectedEntityException {
        return markAs(loan, LoanPayment.Status.IN_PROCESS, LoanPayment.Status.RECOVERED);
    }

    @Override
    public Loan markAsUnrecoverable(final Loan loan) throws UnexpectedEntityException {
        return markAs(loan, LoanPayment.Status.IN_PROCESS, LoanPayment.Status.UNRECOVERABLE);
    }

    @Override
    public TransactionSummaryVO paymentsSummary(final LoanPaymentQuery query) {
        final Status status = query.getStatus();
        if (status == null) {
            throw new ValidationException("status", "loanPayment.status", new RequiredError());
        }
        return loanPaymentDao.paymentsSummary(query);
    }

    @Override
    public Transfer repay(final RepayLoanDTO params) {
        return transactionHelper.maybeRunInNewTransaction(new Transactional<Transfer>() {

            @Override
            public Transfer afterCommit(final Transfer result) {
                return fetchService.fetch(result);
            }

            @Override
            public Transfer doInTransaction(final TransactionStatus status) {
                return doRepay(params);
            }

        });
    }

    @Override
    public List<LoanPayment> search(final LoanPaymentQuery query) {
        return loanPaymentDao.search(query);
    }

    @Override
    public List<Loan> search(final LoanQuery query) {
        if (query.getQueryStatus() == null) {
            query.setHideAuthorizationRelated(
                    !permissionService.hasPermission(AdminMemberPermission.LOANS_VIEW_AUTHORIZED));
        }
        return loanDao.search(query);
    }

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

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

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

    public void setLoanDao(final LoanDAO loanDao) {
        this.loanDao = loanDao;
    }

    public void setLoanPaymentDao(final LoanPaymentDAO loanPaymentDao) {
        this.loanPaymentDao = loanPaymentDao;
    }

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

    public void setMultiPaymentHandler(final LoanHandler handler) {
        handlersByType.put(Loan.Type.MULTI_PAYMENT, handler);
    }

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

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

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

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

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

    public void setSinglePaymentHandler(final LoanHandler handler) {
        handlersByType.put(Loan.Type.SINGLE_PAYMENT, handler);
    }

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

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

    public void setWithInterestHandler(final LoanHandler handler) {
        handlersByType.put(Loan.Type.WITH_INTEREST, handler);
    }

    @Override
    public void validate(final GrantLoanDTO params) {
        final Validator validator = getValidator(params);
        if (validator == null) {
            throw new ValidationException("transferType", "loan.type", new InvalidError());
        }
        validator.validate(params);
    }

    private TransactionSummaryVO buildSummary(final LoanQuery query) {
        BigDecimal amount = BigDecimal.ZERO;
        int count = 0;
        final List<Loan> loans = loanDao.search(query);
        for (Loan loan : loans) {
            count++;
            loan = fetchService.fetch(loan, Loan.Relationships.PAYMENTS);
            final List<LoanPayment> payments = loan.getPayments();
            if (payments != null) {
                for (final LoanPayment payment : payments) {
                    amount = amount.add(payment.getRemainingAmount());
                }
            }
        }
        final TransactionSummaryVO ret = new TransactionSummaryVO();
        ret.setCount(count);
        ret.setAmount(amount);
        return ret;
    }

    private LoanPayment doDiscard(final LoanPaymentDTO dto) {
        final Loan loan = fetchService.fetch(dto.getLoan(), Loan.Relationships.PAYMENTS);
        LoanPayment payment = fetchService.fetch(dto.getLoanPayment());
        final Calendar date = dto.getDate() == null ? Calendar.getInstance() : dto.getDate();
        if (payment == null) {
            payment = loan.getFirstOpenPayment();
        }
        if (payment == null) {
            throw new UnexpectedEntityException();
        }
        payment.setStatus(LoanPayment.Status.DISCARDED);
        payment.setRepaymentDate(date);
        return loanPaymentDao.update(payment);
    }

    private Loan doGrant(final GrantLoanDTO params, final boolean newTransaction) {
        validate(params);

        // Fetch and update the transfer type
        final TransferType transferType = fetchService.fetch(params.getTransferType());
        params.setTransferType(transferType);

        // Insert the loan
        final Loan loan = handlersByType.get(params.getLoanType()).buildLoan(params);

        return transactionHelper.maybeRunInNewTransaction(new Transactional<Loan>() {

            @Override
            public Loan afterCommit(final Loan result) {
                return fetchService.fetch(result);
            }

            @Override
            public Loan doInTransaction(final TransactionStatus status) {
                return doGrant(loan, params);
            }
        }, newTransaction);
    }

    private Loan doGrant(Loan loan, final GrantLoanDTO params) {
        final LocalSettings localSettings = settingsService.getLocalSettings();
        final TransferType transferType = params.getTransferType();

        // Create the loan transfer
        final TransferDTO transferDto = new TransferDTO();
        if (params.isAutomatic()) {
            transferDto.setContext(TransactionContext.AUTOMATIC_LOAN);
        } else {
            transferDto.setContext(TransactionContext.LOAN);
        }
        if (params.getDate() != null) {
            transferDto.setDate(params.getDate());
        }
        transferDto.setToOwner(params.getMember());
        transferDto.setFrom(
                accountService.getAccount(new AccountDTO(SystemAccountOwner.instance(), transferType.getFrom())));
        transferDto
                .setTo(accountService.getAccount(new AccountDTO(transferDto.getToOwner(), transferType.getTo())));
        transferDto.setAmount(params.getAmount());
        transferDto.setDescription(params.getDescription());
        transferDto.setTransferType(transferType);
        transferDto.setCustomValues(params.getCustomValues());
        transferDto.setRates(rateService.applyLoan(transferDto, params));
        final Transfer transfer = (Transfer) paymentService.insertWithoutNotification(transferDto);
        if (transfer.getProcessDate() == null && params.getDate() != null
                && DateHelper.daysBetween(params.getDate(), Calendar.getInstance()) != 0) {
            throw new AuthorizedPaymentInPastException();
        }

        // Persist the loan
        loan.setTransfer(transfer);
        final List<LoanPayment> payments = loan.getPayments();
        loan = loanDao.insert(loan);
        loan.setPayments(new ArrayList<LoanPayment>());

        // Insert the installments
        int index = 0;
        BigDecimal total = BigDecimal.ZERO;
        for (final LoanPayment payment : payments) {
            payment.setLoan(loan);
            payment.setIndex(index++);
            BigDecimal amount = localSettings.round(payment.getAmount());
            if (index == payments.size()) {
                // The last payment should round to total amount
                amount = localSettings.round(loan.getTotalAmount().subtract(total));
            } else {
                total = total.add(amount);
            }
            payment.setAmount(amount);
            loan.getPayments().add(loanPaymentDao.insert(payment));
        }

        // Notify
        memberNotificationHandler.grantedLoanNotification(loan);

        return loan;
    }

    private Transfer doRepay(final RepayLoanDTO params) {
        BigDecimal amount = params.getAmount();

        // Check if the amount is valid
        if (amount.compareTo(paymentService.getMinimumPayment()) < 0) {
            throw new ValidationException("amount", "loan.amount", new InvalidError());
        }

        // Get the loan payment to repay
        Calendar date = params.getDate();
        if (date == null) {
            date = Calendar.getInstance();
            params.setDate(date);
        }
        final LoanRepaymentAmountsDTO amountsDTO = getLoanPaymentAmount(params);
        final LoanPayment payment = amountsDTO.getLoanPayment();
        if (payment == null) {
            throw new UnexpectedEntityException();
        }

        // Validate the amount
        final BigDecimal remainingAmount = amountsDTO.getRemainingAmountAtDate();
        final BigDecimal diff = remainingAmount.subtract(amount);
        final MutableBoolean totallyRepaid = new MutableBoolean();
        // If the amount is on an acceptable delta, set the transfer value = parcel value
        if (diff.abs().floatValue() < PRECISION_DELTA) {
            amount = remainingAmount;
            totallyRepaid.setValue(true);
        } else if (diff.compareTo(BigDecimal.ZERO) < 0
                || !params.getLoan().getTransfer().getType().getLoan().getType().allowsPartialRepayments()) {
            throw new ValidationException("amount", "loan.amount", new InvalidError());
        }
        final LocalSettings localSettings = settingsService.getLocalSettings();
        Loan loan = fetchService.fetch(params.getLoan(), Loan.Relationships.PAYMENTS, RelationshipHelper
                .nested(Loan.Relationships.TRANSFER, Payment.Relationships.TO, MemberAccount.Relationships.MEMBER),
                Loan.Relationships.TO_MEMBERS);

        // Build the transfers for repayment
        final List<TransferDTO> transfers = handlersByType.get(loan.getParameters().getType())
                .buildTransfersForRepayment(params, amountsDTO);
        Transfer root = null;
        BigDecimal totalAmount = BigDecimal.ZERO;
        for (final TransferDTO dto : transfers) {
            if (dto.getAmount().floatValue() < PRECISION_DELTA) {
                // If the root amount is zero, it means that the parent transfer should be the last transfer for this loan payment
                final TransferQuery tq = new TransferQuery();
                tq.setLoanPayment(payment);
                tq.setReverseOrder(true);
                tq.setUniqueResult();
                final List<Transfer> paymentTransfers = paymentService.search(tq);
                if (paymentTransfers.isEmpty()) {
                    throw new IllegalStateException(
                            "The root transfer has amount 0 and there is no other transfers for this payment");
                }
                root = paymentTransfers.iterator().next();
            } else {
                totalAmount = totalAmount.add(dto.getAmount());
                dto.setParent(root);
                dto.setLoanPayment(payment);
                final Transfer transfer = (Transfer) paymentService.insertWithoutNotification(dto);
                if (root == null) {
                    // The first will be the root. All others are it's children
                    root = transfer;
                }
            }
        }

        // Update the loan payment
        final BigDecimal totalRepaid = localSettings.round(payment.getRepaidAmount().add(totalAmount));
        payment.setRepaidAmount(totalRepaid);
        if (totallyRepaid.booleanValue()) {
            // Mark the payment as repaid, if is the case
            payment.setStatus(LoanPayment.Status.REPAID);
            payment.setRepaymentDate(params.getDate());
        }
        payment.setTransfers(null); // Avoid 2 representations of the transfers collection. It's inverse="true", no problem setting null
        loanPaymentDao.update(payment);

        // Return the generated root transfer
        return root;
    }

    private Validator getProjectionValidator() {
        final Validator projectionValidator = new Validator("loan");
        projectionValidator.property("transferType").key("loan.type").required().add(new LoanTypeValidation());
        projectionValidator.property("amount").required().positiveNonZero();
        projectionValidator.property("firstExpirationDate").future().required();
        projectionValidator.property("paymentCount").required().positiveNonZero();
        return projectionValidator;
    }

    private Validator getValidator(final GrantLoanDTO params) {
        // The transfer type is implicitly validated by returning null on non-loan types
        final TransferType transferType = fetchService.fetch(params.getTransferType());
        Loan.Type type;
        try {
            type = transferType.getLoan().getType();
        } catch (final Exception e) {
            return null;
        }

        final Validator validator = new Validator("loan");
        validator.property("amount").required().positiveNonZero();
        final Currency currency = fetchService.fetch(transferType.getCurrency(),
                Currency.Relationships.A_RATE_PARAMETERS, Currency.Relationships.D_RATE_PARAMETERS);
        if (currency.isEnableARate() || currency.isEnableDRate()) {
            // if the date is not null, it might be a payment in past, which is not allowed with rates enabled.
            if (params.getDate() != null) {
                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);
                if (params.getDate().before(now)) {
                    validator.general(new GeneralValidation() {
                        private static final long serialVersionUID = -7221645724425619586L;

                        @Override
                        public ValidationError validate(final Object object) {
                            return new ValidationError("payment.error.pastDateWithRates");
                        }
                    });
                }
            }
        } else {
            validator.property("date").key("loan.grant.manualDate").pastOrToday();
        }
        validator.property("description").required().maxLength(1000);
        validator.property("member").key("member.member").required();
        switch (type) {
        case SINGLE_PAYMENT:
            validator.property("repaymentDate").required();
            break;
        case MULTI_PAYMENT:
            validator.property("payments").required().add(new MultiPaymentValidation());
            break;
        case WITH_INTEREST:
            validator.property("firstRepaymentDate").future().required();
            validator.property("paymentCount").required().positiveNonZero();
        }
        // Custom fields
        validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() {
            @Override
            public Validator getValidator() {
                return paymentCustomFieldService.getValueValidator(transferType);
            }
        }));
        return validator;
    }

    private Loan markAs(Loan loan, final LoanPayment.Status expectedStatus, final LoanPayment.Status newStatus) {
        loan = fetchService.fetch(loan, Loan.Relationships.PAYMENTS);
        for (final LoanPayment current : loan.getPayments()) {
            if (current.getStatus() == expectedStatus) {
                current.setStatus(newStatus);
                loanPaymentDao.update(current);
            }
        }
        return fetchService.reload(loan);
    }

}