package org.mifos.clientportfolio.newloan.domain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.mifos.accounts.loan.business.LoanScheduleEntity;
import org.mifos.accounts.loan.business.RepaymentTotals;
import org.mifos.accounts.productdefinition.util.helpers.GraceType;
import org.mifos.accounts.productdefinition.util.helpers.InterestType;
import org.mifos.config.AccountingRules;
import org.mifos.framework.util.helpers.Money;
/**
* I am responsible for applying rounding rules from AccountingRules.
*
* <h2>Summary of rounding rules</h2> There are two factors that make up a rounding rule: precision and mode.
*
* <dl>
* <dt> <em>Precision</em>
* <dd>specifies the degree of rounding, to the closest decimal place. Example: 1 (closest Rupee), 0.5 (closest
* half-dollar, for example), 0.1, 0.01 (closest US penny) 0.001, etc. Precision is limited by the currency being
* used by the application. For example, US dollars limit the precision to two decimal places (closest penny).
*
* <dt> <em>Mode</em>
* <dd>specfies how rounding occurs. Currently three modes are supported: HALF_UP, FLOOR, CEILING.
* </dl>
*
* Three installment-rounding conventions apply to loan-installment payments. Each specifies the precision and mode
* to be applied in certain contexts:
*
* <dl>
* <dt> <em>Currency-rounding</em>
* <dd>Round to the precision of the currency being used by the application.
*
* <dt> <em>Initial rounding</em>
* <dd>Round all installment's total payment but the last.
*
* <dt> <em>Final rounding</em>
* <dd>Round the last payment to a specified precision.
* </dl>
*
*
* <h2>Summary of rounding and adjustment logic</h2>
*
* Assume we've calculated exact values for each installment's principal, interest, and fees payment, and
* installment's total payment (their sum).
* <p/>
* The concept here is that exact values will be rounded and the amounts that the customer actually pays will drift
* away from what's actually due, resulting in the components of each installment not exactly adding up to the total
* payment.
* <p/>
* Generally, within each installment but the last, the principal payment is the "fall guy", making up for any
* difference. For the last installment, the interest payment is the fall guy.
* <p/>
* Differences in total paid across all installments are made up in the last installment.
* <p/>
* <h4>Rounding and adjusting total payments</h4>
* First compute the rounded and adjusted totals for the loan. These are used to adjust the final installment's
* payments.
* <ul>
* <li>Round the loan's exact total payments (sum of exact principal, exact interest, exact fees) using final
* rounding.
* <li>No need to round the principal, since it is entered using precision of the prevailing currency.
* <li>Round total fees using currency rounding
* <li>Adjust the total interest so that rounded fees, principal, and adjusted interest sum to the rounded total
* payments.
* </ul>
* </ul>
* <h4>Non-grace-period installments except the last:</h4>
* <ul>
* <li>Round the installment's exact total payment using initial rounding.
* <li>Round the installment's exact interest and fee payments using currency rounding.
* <li>Round each of the installment's account fees using currency rounding.
* <li>Adjust the installment's principal to make up the difference between the installment's rounded total payment
* and its rounded interest and fee payments.
* <li>After rounding and adjusting, the installment's (rounded) total payment is exactly the sum of (rounded)
* principal, interest and fees.
* </ul>
*
* <h4>The last installment:</h4>
* <ul>
* <li>Correct for over- or underpayment of prior installment's payments due to rounding:
* <ul>
* <li>Compute the loan's exact total payment as the sum of all installment's exact principal, interest and fees.
* <li>Round the loan's exact total payment using final rounding. This is what the customer must pay to pay off the
* loan.
* <li>Set the final installment's total payment to the difference between the loan's rounded total payment and the
* sum of all prior installments' (already rounded) payments.
* </ul>
* <li>Correct for over or underpayment of principal. Set the last installment's principal payment to the difference
* between the loan amount and the sum of all prior installment's principal payments, then round it using currency
* rounding rules.
* <li>Correct for over- or underpayment of fees:
* <ul>
* <li>Round the exact total fees using Currency rounding rules.
* <li>Set one of last installment's fee payments to the difference between the rounded total fees and the sum of
* all prior installments' (already rounded) fee payments.
* </ul>
* <li>Finally, adjust the last installment's interest payment as the difference between the last installment's
* total payment and the sum of the last installment's principal and fee payments.
* </ul>
*
* <h4>Principal-only grace-period installments</h4>
*
* The principal is always zero, and only interest and fees are paid. Here, interest is the "fall guy", absorbing
* any rounding discrepancies:
* <ul>
* <li>Round the installment's total payments as above.
* <li>Round the installment's fee payment as above.
* <li>Adjust the interest to force interest and fee payments to add up to the installment's total payment.
* </ul>
* <h4>Principal + interest grace-period installments</h4>
*
* Calculations are the same as if there were no grace, since the zero-payment installments are not included in the
* installment list at all.
*/
public class DefaultLoanScheduleRounder implements LoanScheduleRounder {
protected final LoanScheduleRounderHelper loanScheduleInstallmentRounder;
public DefaultLoanScheduleRounder(LoanScheduleRounderHelper loanScheduleInstallmentRounder) {
this.loanScheduleInstallmentRounder = loanScheduleInstallmentRounder;
}
@Override
public List<LoanScheduleEntity> round(GraceType graceType, Short gracePeriodDuration, Money loanAmount,
InterestType interestType,
List<LoanScheduleEntity> unroundedLoanSchedules,
List<LoanScheduleEntity> allExistingLoanSchedules) {
Collections.sort(unroundedLoanSchedules);
List<LoanScheduleEntity> roundedLoanSchedules = new ArrayList<LoanScheduleEntity>();
RepaymentTotals totals = loanScheduleInstallmentRounder.calculateInitialTotals_v2(unroundedLoanSchedules, loanAmount, allExistingLoanSchedules);
int installmentNum = 0;
for (Iterator<LoanScheduleEntity> it = unroundedLoanSchedules.iterator(); it.hasNext();) {
LoanScheduleEntity currentInstallment = it.next();
LoanScheduleEntity roundedInstallment = currentInstallment;
installmentNum++;
if (it.hasNext()) { // handle all but the last installment
if (loanScheduleInstallmentRounder.isGraceInstallment_v2(installmentNum, graceType, gracePeriodDuration)) {
roundedInstallment = loanScheduleInstallmentRounder.roundAndAdjustGraceInstallment_v2(roundedInstallment);
} else if (interestType.equals(InterestType.DECLINING_EPI)) {
loanScheduleInstallmentRounder.roundAndAdjustNonGraceInstallmentForDecliningEPI_v2(roundedInstallment);
} else {
loanScheduleInstallmentRounder.roundAndAdjustButLastNonGraceInstallment_v2(roundedInstallment);
}
loanScheduleInstallmentRounder.updateRunningTotals_v2(totals, roundedInstallment);
} else {
loanScheduleInstallmentRounder.roundAndAdjustLastInstallment_v2(roundedInstallment, totals);
}
roundedLoanSchedules.add(roundedInstallment);
} // for
return roundedLoanSchedules;
}
}