/*
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.List;
import nl.strohalm.cyclos.entities.accounts.AccountOwner;
import nl.strohalm.cyclos.entities.accounts.AccountType;
import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner;
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.LoanRepaymentAmountsDTO;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferType;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.services.accounts.AccountDTO;
import nl.strohalm.cyclos.services.transactions.exceptions.PartialInterestsAmountException;
import nl.strohalm.cyclos.utils.DateHelper;
import nl.strohalm.cyclos.utils.RelationshipHelper;
/**
* Handler for single payment loans
* @author luis
*/
public class LoanWithInterestHandler extends BaseMultiplePaymentLoanHandler {
public LoanWithInterestHandler() {
super(Loan.Type.SINGLE_PAYMENT);
}
@Override
public List<TransferDTO> buildTransfersForRepayment(final RepayLoanDTO repayDTO, final LoanRepaymentAmountsDTO amountsDTO) {
// The super implementation will have a single transfer
final List<TransferDTO> transfers = new ArrayList<TransferDTO>(super.buildTransfersForRepayment(repayDTO, amountsDTO));
final LocalSettings localSettings = settingsService.getLocalSettings();
// Get some required data
final TransferDTO baseRepayment = transfers.get(0);
final Loan loan = repayDTO.getLoan();
final Calendar repaymentDate = repayDTO.getDate() == null ? Calendar.getInstance() : repayDTO.getDate();
final int paymentCount = loan.getPaymentCount();
final Transfer loanTransfer = loan.getTransfer();
final BigDecimal originalAmount = loanTransfer.getAmount();
final LoanParameters parameters = loanTransfer.getType().getLoan();
final LoanPayment payment = baseRepayment.getLoanPayment();
final BigDecimal paymentAmount = payment.getAmount();
final BigDecimal repaidAmount = payment.getRepaidAmount();
final BigDecimal amountToRepay = repayDTO.getAmount();
// Build a transfer for each amount part, as following:
// Imagine the loan amount = 1000, grant fee = 30, monthly interest = 20, in 10 payments = 10 x 105
// Each payment should generate:
// * A base repayment of 100 (original amount repayment, excluding fees and interests)
// * The grant fee repayment of 3
// * The monthly interest repayment of 2
// * When expired, ie: expiration fee = 4, expiring 5 days of 1%/day
// *** The expiration fee repayment of 4
// *** The expiration daily interest repayment of 5 (1% of 100 * 5 days)
final MathContext mathContext = localSettings.getMathContext();
final BigDecimal baseAmount = localSettings.round(originalAmount.divide(new BigDecimal(paymentCount), mathContext));
final BigDecimal totalExtraAmount = paymentAmount.subtract(baseAmount);
final BigDecimal extraAmount = localSettings.round(amountToRepay.add(repaidAmount).subtract(baseAmount));
if (extraAmount.compareTo(BigDecimal.ZERO) == 1) {
// When what is already repaid + this payment amount is greater than the payment without interest or fees,
// there are parts of the amount that are interest or fees. Those parts must be charged on separate transfers
baseRepayment.setAmount(baseAmount.subtract(repaidAmount));
// Allow partial payment only if amount is the total loan amount or less or equals than
// base amount. That's to avoid: baseAmount = 200, extraAmount = 2, the user pays 201.
// How should we interpret the extra 1?!? grantFee? monthlyInterest?
// We just avoid the problem by not allowing it to happen ;-)
if (totalExtraAmount.subtract(extraAmount).floatValue() > PRECISION_DELTA) {
throw new PartialInterestsAmountException(baseAmount.subtract(repaidAmount), paymentAmount.subtract(baseAmount));
}
// Calculate the grant fee repayment
final BigDecimal totalGrantFee = localSettings.round(parameters.calculateGrantFee(originalAmount));
final BigDecimal paymentGrantFee = componentAmount(payment, totalGrantFee);
if (generateComponentTransferForAmount(paymentGrantFee)) {
final TransferDTO transfer = generateComponentTransfer(payment, baseRepayment, parameters, parameters.getGrantFeeRepaymentType(), paymentGrantFee);
transfers.add(transfer);
}
// Calculate the monthly interest repayment
final Calendar firstExpirationDate = loan.getPayments().get(0).getExpirationDate();
final BigDecimal totalMonthlyInterest = localSettings.round(parameters.calculateMonthlyInterests(originalAmount, paymentCount, loanTransfer.getDate(), firstExpirationDate, mathContext));
final BigDecimal paymentMonthlyInterest = componentAmount(payment, totalMonthlyInterest);
if (generateComponentTransferForAmount(paymentMonthlyInterest)) {
final TransferDTO transfer = generateComponentTransfer(payment, baseRepayment, parameters, parameters.getMonthlyInterestRepaymentType(), paymentMonthlyInterest);
transfers.add(transfer);
}
// Calculate expiration fee / interest
final int diff = DateHelper.daysBetween(payment.getExpirationDate(), repaymentDate);
// Expiration fee
final BigDecimal paymentExpirationFee = localSettings.round(parameters.calculatePaymentExpirationFee(paymentAmount, diff));
if (generateComponentTransferForAmount(paymentExpirationFee)) {
final TransferDTO transfer = generateComponentTransfer(payment, baseRepayment, parameters, parameters.getExpirationFeeRepaymentType(), paymentExpirationFee);
transfers.add(transfer);
}
// Expiration interest
final BigDecimal paymentExpirationInterest = localSettings.round(parameters.calculatePaymentExpirationInterest(paymentAmount, diff, mathContext));
if (generateComponentTransferForAmount(paymentExpirationInterest)) {
final TransferDTO transfer = generateComponentTransfer(payment, baseRepayment, parameters, parameters.getExpirationDailyInterestRepaymentType(), paymentExpirationInterest);
transfers.add(transfer);
}
}
return transfers;
}
@Override
protected void processGrant(final Loan loan, final GrantLoanDTO params) {
final Calendar grantDate = DateHelper.truncate(params.getDate() == null ? Calendar.getInstance() : params.getDate());
final GrantLoanWithInterestDTO dto = (GrantLoanWithInterestDTO) params;
final Calendar firstExpirationDate = DateHelper.truncate(dto.getFirstRepaymentDate());
final int parcelCount = dto.getPaymentCount();
final LocalSettings localSettings = settingsService.getLocalSettings();
final MathContext mathContext = localSettings.getMathContext();
// Calculate the total amount
final BigDecimal totalAmount = loan.getParameters().calculateLoanTotal(dto.getAmount(), parcelCount, grantDate, firstExpirationDate, mathContext);
// Build the parcels
final List<LoanPayment> payments = new ArrayList<LoanPayment>(parcelCount);
final BigDecimal parcelAmount = localSettings.round(totalAmount.divide(new BigDecimal(parcelCount), mathContext));
for (int i = 0; i < parcelCount; i++) {
final LoanPayment payment = new LoanPayment();
final Calendar expiration = (Calendar) firstExpirationDate.clone();
expiration.add(Calendar.MONTH, i);
payment.setExpirationDate(expiration);
payment.setAmount(parcelAmount);
payments.add(payment);
}
loan.setTotalAmount(totalAmount);
loan.setPayments(payments);
}
@Override
protected BigDecimal retrieveTotalAmount(final ProjectionDTO params) {
// Calculate the delay
final Calendar grantDate = params.getDate();
final Calendar firstRepaymentDate = params.getFirstExpirationDate();
// Calculate the loan total
final int paymentCount = params.getPaymentCount();
final BigDecimal amount = params.getAmount();
final MathContext mathContext = settingsService.getLocalSettings().getMathContext();
return params.getTransferType().getLoan().calculateLoanTotal(amount, paymentCount, grantDate, firstRepaymentDate, mathContext);
}
/**
* Returns a component amount for the given payment. For example: grant fee = 10.01, for 10 payments, it will be 1.00 for the first 9 and 1.01 for
* the last one
*/
private BigDecimal componentAmount(final LoanPayment payment, final BigDecimal totalComponentAmount) {
if (totalComponentAmount.floatValue() < PRECISION_DELTA) {
return BigDecimal.ZERO;
}
final int paymentCount = payment.getLoan().getPaymentCount();
final int number = payment.getNumber();
final boolean isLast = number == paymentCount;
final LocalSettings localSettings = settingsService.getLocalSettings();
final MathContext mathContext = localSettings.getMathContext();
final BigDecimal perPaymentAmount = localSettings.round(totalComponentAmount.divide(new BigDecimal(paymentCount), mathContext));
if (isLast) {
return totalComponentAmount.subtract(perPaymentAmount.multiply(new BigDecimal(number).subtract(new BigDecimal(1))));
} else {
return perPaymentAmount;
}
}
/**
* Return a transfer for the given parameters, or null if not applicable
*/
private TransferDTO generateComponentTransfer(final LoanPayment payment, final TransferDTO baseRepayment, final LoanParameters parameters, TransferType transferType, final BigDecimal amount) {
final Loan loan = payment.getLoan();
AccountOwner fromOwner;
AccountOwner toOwner;
transferType = fetchService.fetch(transferType, RelationshipHelper.nested(TransferType.Relationships.FROM, AccountType.Relationships.CURRENCY), TransferType.Relationships.TO);
if (transferType.isFromSystem()) {
fromOwner = SystemAccountOwner.instance();
toOwner = loan.getMember();
} else {
fromOwner = loan.getMember();
toOwner = SystemAccountOwner.instance();
}
final TransferDTO transfer = new TransferDTO();
transfer.setAutomatic(true);
transfer.setDate(baseRepayment.getDate());
transfer.setTransferType(transferType);
transfer.setFrom(accountService.getAccount(new AccountDTO(fromOwner, transferType.getFrom())));
transfer.setTo(accountService.getAccount(new AccountDTO(toOwner, transferType.getTo())));
transfer.setAmount(amount);
transfer.setDescription(buildDescriptionForRepayment(transferType, payment));
return transfer;
}
/**
* Check if the given amount is greater than the minimum payment
*/
private boolean generateComponentTransferForAmount(final BigDecimal amount) {
return amount.subtract(paymentService.getMinimumPayment()).floatValue() > PRECISION_DELTA;
}
}