work on fixing interest calculation for declining balance loans.
This commit is contained in:
@@ -11,7 +11,7 @@ public class CurrencyData implements Serializable {
|
|||||||
private final String nameCode;
|
private final String nameCode;
|
||||||
private final String displayLabel;
|
private final String displayLabel;
|
||||||
|
|
||||||
public CurrencyData(final String code, final String name, final int decimalPlaces, String displaySymbol, String nameCode) {
|
public CurrencyData(final String code, final String name, final int decimalPlaces, final String displaySymbol, final String nameCode) {
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.decimalPlaces = decimalPlaces;
|
this.decimalPlaces = decimalPlaces;
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package org.mifosng.platform.loanschedule.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.joda.time.LocalDate;
|
||||||
|
import org.mifosng.platform.api.data.CurrencyData;
|
||||||
|
import org.mifosng.platform.api.data.LoanSchedule;
|
||||||
|
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
|
||||||
|
|
||||||
|
public interface AmortizationLoanScheduleGenerator {
|
||||||
|
|
||||||
|
LoanSchedule generate(LoanProductRelatedDetail loanScheduleInfo, LocalDate disbursementDate,
|
||||||
|
LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom, CurrencyData currencyData,
|
||||||
|
BigDecimal periodInterestRateForRepaymentPeriod,
|
||||||
|
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate,
|
||||||
|
List<LocalDate> scheduledDates);
|
||||||
|
|
||||||
|
}
|
||||||
+27
-106
@@ -1,7 +1,6 @@
|
|||||||
package org.mifosng.platform.loanschedule.domain;
|
package org.mifosng.platform.loanschedule.domain;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -13,16 +12,31 @@ import org.mifosng.platform.api.data.MoneyData;
|
|||||||
import org.mifosng.platform.api.data.ScheduledLoanInstallment;
|
import org.mifosng.platform.api.data.ScheduledLoanInstallment;
|
||||||
import org.mifosng.platform.currency.domain.MonetaryCurrency;
|
import org.mifosng.platform.currency.domain.MonetaryCurrency;
|
||||||
import org.mifosng.platform.currency.domain.Money;
|
import org.mifosng.platform.currency.domain.Money;
|
||||||
|
import org.mifosng.platform.loan.domain.AmortizationMethod;
|
||||||
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
|
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
|
||||||
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
|
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
|
||||||
|
|
||||||
public class DecliningBalanceMethodLoanScheduleGenerator implements
|
/**
|
||||||
LoanScheduleGenerator {
|
* <p>
|
||||||
|
* Declining balance can amortized (see {@link AmortizationMethod}) in two ways at present:
|
||||||
|
* <ol><li>Equal principal payments</li><li>Equal installment payments</li></ol>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* When amortized using <i>equal principal payments</i>, the <b>principal component</b> of each installment is fixed and <b>interest due</b>
|
||||||
|
* is calculated from the <b>outstanding principal balance</b> resulting in a different <b>total payment due</b> for each installment.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* When amortized using <i>equal installments</i>, the <b>total payment due</b> for each installment is fixed and
|
||||||
|
* is calculated using the excel like <code>pmt</code> function. The <b>interest due</b> is calculated from the <b>outstanding principal balance</b>
|
||||||
|
* which results in a <b>principal component</b> that is <b>total payment due</b> minus <b>interest due</b>.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class DecliningBalanceMethodLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||||
|
|
||||||
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
|
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
|
||||||
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
|
|
||||||
private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator();
|
private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator();
|
||||||
private final PmtCalculator pmtCalculator = new PmtCalculator();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoanSchedule generate(
|
public LoanSchedule generate(
|
||||||
@@ -31,114 +45,21 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements
|
|||||||
final PeriodFrequencyType loanTermFrequencyType,
|
final PeriodFrequencyType loanTermFrequencyType,
|
||||||
final LocalDate disbursementDate,
|
final LocalDate disbursementDate,
|
||||||
final LocalDate firstRepaymentDate,
|
final LocalDate firstRepaymentDate,
|
||||||
final LocalDate interestCalculatedFrom, CurrencyData currencyData) {
|
final LocalDate interestCalculatedFrom,
|
||||||
|
final CurrencyData currencyData) {
|
||||||
List<ScheduledLoanInstallment> scheduledLoanInstallments = new ArrayList<ScheduledLoanInstallment>();
|
|
||||||
|
|
||||||
// 1. generate valid set of 'due dates' based on some of the 'loan attributes'
|
// 1. generate valid set of 'due dates' based on some of the 'loan attributes'
|
||||||
List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate);
|
List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate);
|
||||||
|
|
||||||
|
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate(loanScheduleInfo, scheduledDates);
|
||||||
|
|
||||||
// 2. determine the 'periodic' interest rate based on the 'repayment periods' so we can use
|
// 2. determine the 'periodic' interest rate based on the 'repayment periods' so we can use
|
||||||
final BigDecimal periodInterestRateForRepaymentPeriod = this.periodicInterestRateCalculator.calculateFrom(loanScheduleInfo);
|
final BigDecimal periodInterestRateForRepaymentPeriod = this.periodicInterestRateCalculator.calculateFrom(loanScheduleInfo);
|
||||||
|
|
||||||
// 3. determine 'total payment' for each repayment based on pmt function (and hence the total due overall)
|
// Determine with 'amortisation' approach to use
|
||||||
final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency();
|
AmortizationLoanScheduleGenerator generator = new EqualInstallmentsAmortizationLoanScheduleGenerator();
|
||||||
Money totalDuePerInstallment = this.pmtCalculator.calculatePaymentForOnePeriodFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
|
return generator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData,
|
||||||
// final Money totalDue = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
|
periodInterestRateForRepaymentPeriod, idealDisbursementDateBasedOnFirstRepaymentDate, scheduledDates);
|
||||||
|
|
||||||
// REVIEW. calculate total interest due based on loan term settings rather than inferring loan term from regular repayment schedule
|
|
||||||
final BigDecimal periodInterestRateForLoanTermPeriod = this.periodicInterestRateCalculator.calculateFrom(loanTermFrequencyType, loanScheduleInfo.getAnnualNominalInterestRate());
|
|
||||||
final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo.getPrincipal(), loanTermFrequency, periodInterestRateForLoanTermPeriod, monetaryCurrency);
|
|
||||||
|
|
||||||
totalDuePerInstallment = totalRepaymentDueForLoanTerm.dividedBy(loanScheduleInfo.getNumberOfRepayments(), RoundingMode.HALF_EVEN);
|
|
||||||
|
|
||||||
Money totalInterestDue = totalRepaymentDueForLoanTerm.minus(loanScheduleInfo.getPrincipal());
|
|
||||||
Money outstandingBalance = loanScheduleInfo.getPrincipal();
|
|
||||||
Money totalPrincipal = Money.zero(monetaryCurrency);
|
|
||||||
Money totalInterest = Money.zero(monetaryCurrency);
|
|
||||||
|
|
||||||
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate(loanScheduleInfo, scheduledDates);
|
|
||||||
double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator.calculateRepaymentPeriodAsAFractionOfDays(loanScheduleInfo.getRepaymentPeriodFrequencyType(),
|
|
||||||
loanScheduleInfo.getRepayEvery(), interestCalculatedFrom, scheduledDates, idealDisbursementDateBasedOnFirstRepaymentDate);
|
|
||||||
|
|
||||||
LocalDate startDate = disbursementDate;
|
|
||||||
int installmentNumber = 1;
|
|
||||||
for (LocalDate scheduledDueDate : scheduledDates) {
|
|
||||||
|
|
||||||
// number of days from startDate to this scheduledDate
|
|
||||||
int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()).getDays();
|
|
||||||
|
|
||||||
// TODO - total interest for loan is calculated using loan term settings (loanTermFrequency & loanTermFrequencyType)
|
|
||||||
// - its possible that the 'repyament schedule' is irregular in which case the interest for the installment might not be the periodic interest rate
|
|
||||||
// - but infact somethink like twice it e.g. 4 month installment followed by 2 month installment followed by 1 month installment
|
|
||||||
// - the below doesnt really support highly irregular loan schedules and this case needs to be looked at.
|
|
||||||
Money interestForInstallment = this.periodicInterestRateCalculator.calculateInterestOn(outstandingBalance, periodInterestRateForRepaymentPeriod, daysInPeriod, loanScheduleInfo);
|
|
||||||
Money principalForInstallment = this.periodicInterestRateCalculator.calculatePrincipalOn(totalDuePerInstallment, interestForInstallment, loanScheduleInfo);
|
|
||||||
|
|
||||||
if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) {
|
|
||||||
Money graceOnInterestForRepaymentPeriod = interestForInstallment;
|
|
||||||
interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod);
|
|
||||||
totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod);
|
|
||||||
interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction - Integer.valueOf(1).doubleValue();
|
|
||||||
} else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) {
|
|
||||||
Money graceOnInterestForRepaymentPeriod = interestForInstallment.multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction);
|
|
||||||
interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod);
|
|
||||||
totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod);
|
|
||||||
interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0");
|
|
||||||
}
|
|
||||||
|
|
||||||
totalPrincipal = totalPrincipal.plus(principalForInstallment);
|
|
||||||
totalInterest = totalInterest.plus(interestForInstallment);
|
|
||||||
|
|
||||||
if (installmentNumber == loanScheduleInfo.getNumberOfRepayments()) {
|
|
||||||
Money principalDifference = totalPrincipal.minus(loanScheduleInfo.getPrincipal());
|
|
||||||
if (principalDifference.isLessThanZero()) {
|
|
||||||
principalForInstallment = principalForInstallment.plus(principalDifference.abs());
|
|
||||||
} else if (principalDifference.isGreaterThanZero()) {
|
|
||||||
principalForInstallment = principalForInstallment.minus(principalDifference.abs());
|
|
||||||
}
|
|
||||||
|
|
||||||
final Money interestDifference = totalInterest.minus(totalInterestDue);
|
|
||||||
if (interestDifference.isLessThanZero()) {
|
|
||||||
interestForInstallment = interestForInstallment.plus(interestDifference.abs());
|
|
||||||
} else if (interestDifference.isGreaterThanZero()) {
|
|
||||||
interestForInstallment = interestForInstallment.minus(interestDifference.abs());
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (loanScheduleInfo.getAmortizationMethod().equals(AmortizationMethod.EQUAL_INSTALLMENTS)) {
|
|
||||||
// Money interestDifference = totalInterest.minus(totalInterestDue);
|
|
||||||
// if (interestDifference.isLessThanZero()) {
|
|
||||||
// interestForInstallment = interestForInstallment.plus(interestDifference.abs());
|
|
||||||
// } else if (interestDifference.isGreaterThanZero()) {
|
|
||||||
// interestForInstallment = interestForInstallment.minus(interestDifference.abs());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment);
|
|
||||||
|
|
||||||
outstandingBalance = outstandingBalance.minus(principalForInstallment);
|
|
||||||
|
|
||||||
MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalForInstallment.getAmount());
|
|
||||||
MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestForInstallment.getAmount());
|
|
||||||
MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount());
|
|
||||||
MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount());
|
|
||||||
|
|
||||||
ScheduledLoanInstallment installment = new ScheduledLoanInstallment(
|
|
||||||
Integer.valueOf(installmentNumber), startDate,
|
|
||||||
scheduledDueDate, principalPerInstallmentValue,
|
|
||||||
interestPerInstallmentValue,
|
|
||||||
totalInstallmentDueValue,
|
|
||||||
outstandingBalanceValue);
|
|
||||||
|
|
||||||
scheduledLoanInstallments.add(installment);
|
|
||||||
|
|
||||||
startDate = scheduledDueDate;
|
|
||||||
|
|
||||||
installmentNumber++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new LoanSchedule(scheduledLoanInstallments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
package org.mifosng.platform.loanschedule.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.joda.time.Days;
|
||||||
|
import org.joda.time.LocalDate;
|
||||||
|
import org.mifosng.platform.api.data.CurrencyData;
|
||||||
|
import org.mifosng.platform.api.data.LoanSchedule;
|
||||||
|
import org.mifosng.platform.api.data.MoneyData;
|
||||||
|
import org.mifosng.platform.api.data.ScheduledLoanInstallment;
|
||||||
|
import org.mifosng.platform.currency.domain.MonetaryCurrency;
|
||||||
|
import org.mifosng.platform.currency.domain.Money;
|
||||||
|
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
|
||||||
|
|
||||||
|
public class EqualInstallmentsAmortizationLoanScheduleGenerator implements AmortizationLoanScheduleGenerator {
|
||||||
|
|
||||||
|
private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator();
|
||||||
|
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
|
||||||
|
private final PmtCalculator pmtCalculator = new PmtCalculator();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoanSchedule generate(final LoanProductRelatedDetail loanScheduleInfo,
|
||||||
|
final LocalDate disbursementDate,
|
||||||
|
final LocalDate firstRepaymentDate,
|
||||||
|
final LocalDate interestCalculatedFrom,
|
||||||
|
final CurrencyData currencyData,
|
||||||
|
final BigDecimal periodInterestRateForRepaymentPeriod,
|
||||||
|
final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate,
|
||||||
|
final List<LocalDate> scheduledDates) {
|
||||||
|
|
||||||
|
List<ScheduledLoanInstallment> scheduledLoanInstallments = new ArrayList<ScheduledLoanInstallment>();
|
||||||
|
|
||||||
|
// 3. determine 'total payment' for each repayment based on pmt function (and hence the total due overall)
|
||||||
|
final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency();
|
||||||
|
Money totalDuePerInstallment = this.pmtCalculator.calculatePaymentForOnePeriodFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
|
||||||
|
final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
|
||||||
|
|
||||||
|
Money totalInterestDue = totalRepaymentDueForLoanTerm.minus(loanScheduleInfo.getPrincipal());
|
||||||
|
Money outstandingBalance = loanScheduleInfo.getPrincipal();
|
||||||
|
Money totalPrincipal = Money.zero(monetaryCurrency);
|
||||||
|
Money totalInterest = Money.zero(monetaryCurrency);
|
||||||
|
|
||||||
|
double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator.calculateRepaymentPeriodAsAFractionOfDays(loanScheduleInfo.getRepaymentPeriodFrequencyType(),
|
||||||
|
loanScheduleInfo.getRepayEvery(), interestCalculatedFrom, scheduledDates, idealDisbursementDateBasedOnFirstRepaymentDate);
|
||||||
|
|
||||||
|
LocalDate startDate = disbursementDate;
|
||||||
|
int installmentNumber = 1;
|
||||||
|
for (LocalDate scheduledDueDate : scheduledDates) {
|
||||||
|
|
||||||
|
// number of days from startDate to this scheduledDate
|
||||||
|
int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()).getDays();
|
||||||
|
|
||||||
|
Money interestForInstallment = this.periodicInterestRateCalculator.calculateInterestOn(outstandingBalance, periodInterestRateForRepaymentPeriod, daysInPeriod, loanScheduleInfo);
|
||||||
|
Money principalForInstallment = this.periodicInterestRateCalculator.calculatePrincipalOn(totalDuePerInstallment, interestForInstallment, loanScheduleInfo);
|
||||||
|
|
||||||
|
if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) {
|
||||||
|
Money graceOnInterestForRepaymentPeriod = interestForInstallment;
|
||||||
|
interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod);
|
||||||
|
totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod);
|
||||||
|
interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction - Integer.valueOf(1).doubleValue();
|
||||||
|
} else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) {
|
||||||
|
Money graceOnInterestForRepaymentPeriod = interestForInstallment.multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction);
|
||||||
|
interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod);
|
||||||
|
totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod);
|
||||||
|
interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0");
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPrincipal = totalPrincipal.plus(principalForInstallment);
|
||||||
|
totalInterest = totalInterest.plus(interestForInstallment);
|
||||||
|
|
||||||
|
if (installmentNumber == loanScheduleInfo.getNumberOfRepayments()) {
|
||||||
|
Money principalDifference = totalPrincipal.minus(loanScheduleInfo.getPrincipal());
|
||||||
|
if (principalDifference.isLessThanZero()) {
|
||||||
|
principalForInstallment = principalForInstallment.plus(principalDifference.abs());
|
||||||
|
} else if (principalDifference.isGreaterThanZero()) {
|
||||||
|
principalForInstallment = principalForInstallment.minus(principalDifference.abs());
|
||||||
|
}
|
||||||
|
|
||||||
|
final Money interestDifference = totalInterest.minus(totalInterestDue);
|
||||||
|
if (interestDifference.isLessThanZero()) {
|
||||||
|
interestForInstallment = interestForInstallment.plus(interestDifference.abs());
|
||||||
|
} else if (interestDifference.isGreaterThanZero()) {
|
||||||
|
interestForInstallment = interestForInstallment.minus(interestDifference.abs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment);
|
||||||
|
|
||||||
|
outstandingBalance = outstandingBalance.minus(principalForInstallment);
|
||||||
|
|
||||||
|
MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalForInstallment.getAmount());
|
||||||
|
MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestForInstallment.getAmount());
|
||||||
|
MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount());
|
||||||
|
MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount());
|
||||||
|
|
||||||
|
ScheduledLoanInstallment installment = new ScheduledLoanInstallment(
|
||||||
|
Integer.valueOf(installmentNumber), startDate,
|
||||||
|
scheduledDueDate, principalPerInstallmentValue,
|
||||||
|
interestPerInstallmentValue,
|
||||||
|
totalInstallmentDueValue,
|
||||||
|
outstandingBalanceValue);
|
||||||
|
|
||||||
|
scheduledLoanInstallments.add(installment);
|
||||||
|
|
||||||
|
startDate = scheduledDueDate;
|
||||||
|
|
||||||
|
installmentNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LoanSchedule(scheduledLoanInstallments);
|
||||||
|
}
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
package org.mifosng.platform.loanschedule.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.joda.time.Days;
|
||||||
|
import org.joda.time.LocalDate;
|
||||||
|
import org.mifosng.platform.api.data.CurrencyData;
|
||||||
|
import org.mifosng.platform.api.data.LoanSchedule;
|
||||||
|
import org.mifosng.platform.api.data.MoneyData;
|
||||||
|
import org.mifosng.platform.api.data.ScheduledLoanInstallment;
|
||||||
|
import org.mifosng.platform.currency.domain.MonetaryCurrency;
|
||||||
|
import org.mifosng.platform.currency.domain.Money;
|
||||||
|
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
|
||||||
|
|
||||||
|
public class EqualInstallmentsWithIrregularRepaymentScheduleAmortizationLoanScheduleGenerator implements AmortizationLoanScheduleGenerator {
|
||||||
|
|
||||||
|
private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator();
|
||||||
|
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
|
||||||
|
private final PmtCalculator pmtCalculator = new PmtCalculator();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoanSchedule generate(final LoanProductRelatedDetail loanScheduleInfo,
|
||||||
|
final LocalDate disbursementDate,
|
||||||
|
final LocalDate firstRepaymentDate,
|
||||||
|
final LocalDate interestCalculatedFrom,
|
||||||
|
final CurrencyData currencyData,
|
||||||
|
final BigDecimal periodInterestRateForRepaymentPeriod,
|
||||||
|
final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate,
|
||||||
|
final List<LocalDate> scheduledDates) {
|
||||||
|
|
||||||
|
List<ScheduledLoanInstallment> scheduledLoanInstallments = new ArrayList<ScheduledLoanInstallment>();
|
||||||
|
|
||||||
|
// 3. determine 'total payment' for each repayment based on pmt function (and hence the total due overall)
|
||||||
|
final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency();
|
||||||
|
Money totalDuePerInstallment = this.pmtCalculator.calculatePaymentForOnePeriodFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
|
||||||
|
final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
|
||||||
|
|
||||||
|
// REVIEW. calculate total interest due based on loan term settings rather than inferring loan term from regular repayment schedule
|
||||||
|
// final BigDecimal periodInterestRateForLoanTermPeriod = this.periodicInterestRateCalculator.calculateFrom(loanTermFrequencyType, loanScheduleInfo.getAnnualNominalInterestRate());
|
||||||
|
// final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo.getPrincipal(), loanTermFrequency, periodInterestRateForLoanTermPeriod, monetaryCurrency);
|
||||||
|
|
||||||
|
// totalDuePerInstallment = totalRepaymentDueForLoanTerm.dividedBy(loanScheduleInfo.getNumberOfRepayments(), RoundingMode.HALF_EVEN);
|
||||||
|
|
||||||
|
Money totalInterestDue = totalRepaymentDueForLoanTerm.minus(loanScheduleInfo.getPrincipal());
|
||||||
|
Money outstandingBalance = loanScheduleInfo.getPrincipal();
|
||||||
|
Money totalPrincipal = Money.zero(monetaryCurrency);
|
||||||
|
Money totalInterest = Money.zero(monetaryCurrency);
|
||||||
|
|
||||||
|
double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator.calculateRepaymentPeriodAsAFractionOfDays(loanScheduleInfo.getRepaymentPeriodFrequencyType(),
|
||||||
|
loanScheduleInfo.getRepayEvery(), interestCalculatedFrom, scheduledDates, idealDisbursementDateBasedOnFirstRepaymentDate);
|
||||||
|
|
||||||
|
LocalDate startDate = disbursementDate;
|
||||||
|
int installmentNumber = 1;
|
||||||
|
for (LocalDate scheduledDueDate : scheduledDates) {
|
||||||
|
|
||||||
|
// number of days from startDate to this scheduledDate
|
||||||
|
int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()).getDays();
|
||||||
|
|
||||||
|
// TODO - total interest for loan is calculated using loan term settings (loanTermFrequency & loanTermFrequencyType)
|
||||||
|
// - its possible that the 'repyament schedule' is irregular in which case the interest for the installment might not be the periodic interest rate
|
||||||
|
// - but infact somethink like twice it e.g. 4 month installment followed by 2 month installment followed by 1 month installment
|
||||||
|
// - the below doesnt really support highly irregular loan schedules and this case needs to be looked at.
|
||||||
|
Money interestForInstallment = this.periodicInterestRateCalculator.calculateInterestOn(outstandingBalance, periodInterestRateForRepaymentPeriod, daysInPeriod, loanScheduleInfo);
|
||||||
|
Money principalForInstallment = this.periodicInterestRateCalculator.calculatePrincipalOn(totalDuePerInstallment, interestForInstallment, loanScheduleInfo);
|
||||||
|
|
||||||
|
if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) {
|
||||||
|
Money graceOnInterestForRepaymentPeriod = interestForInstallment;
|
||||||
|
interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod);
|
||||||
|
totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod);
|
||||||
|
interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction - Integer.valueOf(1).doubleValue();
|
||||||
|
} else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) {
|
||||||
|
Money graceOnInterestForRepaymentPeriod = interestForInstallment.multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction);
|
||||||
|
interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod);
|
||||||
|
totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod);
|
||||||
|
interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0");
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPrincipal = totalPrincipal.plus(principalForInstallment);
|
||||||
|
totalInterest = totalInterest.plus(interestForInstallment);
|
||||||
|
|
||||||
|
if (installmentNumber == loanScheduleInfo.getNumberOfRepayments()) {
|
||||||
|
Money principalDifference = totalPrincipal.minus(loanScheduleInfo.getPrincipal());
|
||||||
|
if (principalDifference.isLessThanZero()) {
|
||||||
|
principalForInstallment = principalForInstallment.plus(principalDifference.abs());
|
||||||
|
} else if (principalDifference.isGreaterThanZero()) {
|
||||||
|
principalForInstallment = principalForInstallment.minus(principalDifference.abs());
|
||||||
|
}
|
||||||
|
|
||||||
|
final Money interestDifference = totalInterest.minus(totalInterestDue);
|
||||||
|
if (interestDifference.isLessThanZero()) {
|
||||||
|
interestForInstallment = interestForInstallment.plus(interestDifference.abs());
|
||||||
|
} else if (interestDifference.isGreaterThanZero()) {
|
||||||
|
interestForInstallment = interestForInstallment.minus(interestDifference.abs());
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (loanScheduleInfo.getAmortizationMethod().equals(AmortizationMethod.EQUAL_INSTALLMENTS)) {
|
||||||
|
// Money interestDifference = totalInterest.minus(totalInterestDue);
|
||||||
|
// if (interestDifference.isLessThanZero()) {
|
||||||
|
// interestForInstallment = interestForInstallment.plus(interestDifference.abs());
|
||||||
|
// } else if (interestDifference.isGreaterThanZero()) {
|
||||||
|
// interestForInstallment = interestForInstallment.minus(interestDifference.abs());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment);
|
||||||
|
|
||||||
|
outstandingBalance = outstandingBalance.minus(principalForInstallment);
|
||||||
|
|
||||||
|
MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalForInstallment.getAmount());
|
||||||
|
MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestForInstallment.getAmount());
|
||||||
|
MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount());
|
||||||
|
MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount());
|
||||||
|
|
||||||
|
ScheduledLoanInstallment installment = new ScheduledLoanInstallment(
|
||||||
|
Integer.valueOf(installmentNumber), startDate,
|
||||||
|
scheduledDueDate, principalPerInstallmentValue,
|
||||||
|
interestPerInstallmentValue,
|
||||||
|
totalInstallmentDueValue,
|
||||||
|
outstandingBalanceValue);
|
||||||
|
|
||||||
|
scheduledLoanInstallments.add(installment);
|
||||||
|
|
||||||
|
startDate = scheduledDueDate;
|
||||||
|
|
||||||
|
installmentNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LoanSchedule(scheduledLoanInstallments);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-3
@@ -34,9 +34,11 @@ public class PeriodicInterestRateCalculator {
|
|||||||
return loanScheduleInfo.getAnnualNominalInterestRate().divide(divisor, mc).multiply(numberOfPeriods);
|
return loanScheduleInfo.getAnnualNominalInterestRate().divide(divisor, mc).multiply(numberOfPeriods);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Money calculateInterestOn(Money outstandingBalance, BigDecimal periodInterestRateForRepaymentPeriod,
|
public Money calculateInterestOn(
|
||||||
int daysInPeriod,
|
final Money outstandingBalance,
|
||||||
LoanProductRelatedDetail loanScheduleInfo) {
|
final BigDecimal periodInterestRateForRepaymentPeriod,
|
||||||
|
final int daysInPeriod,
|
||||||
|
final LoanProductRelatedDetail loanScheduleInfo) {
|
||||||
|
|
||||||
MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN);
|
MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN);
|
||||||
Money interestDue = Money.zero(outstandingBalance.getCurrency());
|
Money interestDue = Money.zero(outstandingBalance.getCurrency());
|
||||||
|
|||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
package org.mifosng.platform.loanschedule.domain;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
import org.joda.time.LocalDate;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mifosng.platform.api.data.CurrencyData;
|
||||||
|
import org.mifosng.platform.api.data.LoanSchedule;
|
||||||
|
import org.mifosng.platform.api.data.ScheduledLoanInstallment;
|
||||||
|
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
|
||||||
|
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
|
||||||
|
import org.mockito.runners.MockitoJUnitRunner;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class DecliningBalanceMethodLoanScheduleGeneratorTest {
|
||||||
|
|
||||||
|
// class under test
|
||||||
|
private DecliningBalanceMethodLoanScheduleGenerator decliningBalanceGenerator;
|
||||||
|
|
||||||
|
// test doubles
|
||||||
|
private CurrencyData currencyData = null;
|
||||||
|
private LoanProductRelatedDetail loanScheduleInfo = null;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setupForEachTest() {
|
||||||
|
currencyData = new CurrencyData("USD", "US Dollar", 2, "$", "currency.USD");
|
||||||
|
|
||||||
|
decliningBalanceGenerator = new DecliningBalanceMethodLoanScheduleGenerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenEqualInstallmentAmortisationShouldCalculatePaymentPerInstallmentUsingPmtFunctionAndDerivePrincipalAndInterestComponents() {
|
||||||
|
|
||||||
|
// setup
|
||||||
|
Integer loanTermFrequency = 12;
|
||||||
|
PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.MONTHS;
|
||||||
|
loanScheduleInfo = LoanProductRelatedDetailTestHelper.createSettingsForEqualInstallmentAmortizationQuarterly();
|
||||||
|
|
||||||
|
LocalDate disbursementDate = new LocalDate(2011, 1, 11);
|
||||||
|
LocalDate firstRepaymentDate = new LocalDate(2011, 5, 1);
|
||||||
|
LocalDate interestCalculatedFrom = new LocalDate(2011, 2, 1);
|
||||||
|
|
||||||
|
// pre assertions
|
||||||
|
assertThat(loanTermFrequency, is(loanScheduleInfo.getNumberOfRepayments() * loanScheduleInfo.getRepayEvery()));
|
||||||
|
assertThat(loanTermFrequencyType, is(loanScheduleInfo.getRepaymentPeriodFrequencyType()));
|
||||||
|
|
||||||
|
// exercise test
|
||||||
|
LoanSchedule schedule = decliningBalanceGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType,
|
||||||
|
disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData);
|
||||||
|
|
||||||
|
// verification (post assertions)
|
||||||
|
given(schedule).assertHasInstallmentSizeOf(4);
|
||||||
|
given(schedule).assertTotalPaymentDueOf(1, is("57718.30")).assertInterestDueComponentOf(1, is("12000.00")).assertPrincipalDueComponentOf(1, is("45718.30"));
|
||||||
|
given(schedule).assertTotalPaymentDueOf(2, is("57718.30")).assertInterestDueComponentOf(2, is("9256.90")).assertPrincipalDueComponentOf(2, is("48461.40"));
|
||||||
|
given(schedule).assertTotalPaymentDueOf(3, is("57718.30")).assertInterestDueComponentOf(3, is("6349.22")).assertPrincipalDueComponentOf(3, is("51369.08"));
|
||||||
|
given(schedule).assertTotalPaymentDueOf(4, is("57718.30")).assertInterestDueComponentOf(4, is("3267.08")).assertPrincipalDueComponentOf(4, is("54451.22"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoanScheduleAssertionBuilder given(final LoanSchedule schedule) {
|
||||||
|
return new LoanScheduleAssertionBuilder(schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LoanScheduleAssertionBuilder {
|
||||||
|
|
||||||
|
private final LoanSchedule schedule;
|
||||||
|
|
||||||
|
public LoanScheduleAssertionBuilder(final LoanSchedule schedule) {
|
||||||
|
this.schedule = schedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoanScheduleAssertionBuilder assertPrincipalDueComponentOf(final int installmentNumber, Matcher<String> matchesExpectedAmount) {
|
||||||
|
final Integer installmentIndex = installmentNumber -1;
|
||||||
|
List<ScheduledLoanInstallment> repaymentInstallments = schedule.getScheduledLoanInstallments();
|
||||||
|
ScheduledLoanInstallment repaymentInstallment = repaymentInstallments.get(installmentIndex);
|
||||||
|
|
||||||
|
assertThat("Expected principal due component for installment " + installmentNumber + " is wrong: ",
|
||||||
|
repaymentInstallment.getPrincipalDue().toString(), matchesExpectedAmount);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoanScheduleAssertionBuilder assertInterestDueComponentOf(final int installmentNumber, Matcher<String> matchesExpectedAmount) {
|
||||||
|
final Integer installmentIndex = installmentNumber -1;
|
||||||
|
List<ScheduledLoanInstallment> repaymentInstallments = schedule.getScheduledLoanInstallments();
|
||||||
|
ScheduledLoanInstallment repaymentInstallment = repaymentInstallments.get(installmentIndex);
|
||||||
|
|
||||||
|
assertThat("Expected interest due component for installment " + installmentNumber + " is wrong: ",
|
||||||
|
repaymentInstallment.getInterestDue().toString(), matchesExpectedAmount);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoanScheduleAssertionBuilder assertTotalPaymentDueOf(final int installmentNumber, Matcher<String> matchesExpectedAmount) {
|
||||||
|
final Integer installmentIndex = installmentNumber -1;
|
||||||
|
List<ScheduledLoanInstallment> repaymentInstallments = schedule.getScheduledLoanInstallments();
|
||||||
|
ScheduledLoanInstallment repaymentInstallment = repaymentInstallments.get(installmentIndex);
|
||||||
|
|
||||||
|
assertThat("Expected total payment for installment " + installmentNumber + " is wrong: ",
|
||||||
|
repaymentInstallment.getTotalInstallmentDue().toString(), matchesExpectedAmount);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoanScheduleAssertionBuilder assertHasInstallmentSizeOf(int expectedInstallmentSize) {
|
||||||
|
assertThat("Expected number of installments is wrong: ", schedule.getScheduledLoanInstallments().size(), is(expectedInstallmentSize));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package org.mifosng.platform.loanschedule.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import org.mifosng.platform.currency.domain.MonetaryCurrency;
|
||||||
|
import org.mifosng.platform.loan.domain.AmortizationMethod;
|
||||||
|
import org.mifosng.platform.loan.domain.InterestCalculationPeriodMethod;
|
||||||
|
import org.mifosng.platform.loan.domain.InterestMethod;
|
||||||
|
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
|
||||||
|
import org.mifosng.platform.loan.domain.MonetaryCurrencyBuilder;
|
||||||
|
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to keep in one place configurations for setting up {@link LoanProductRelatedDetail} object used in {@link LoanScheduleGenerator}'s
|
||||||
|
*/
|
||||||
|
public class LoanProductRelatedDetailTestHelper {
|
||||||
|
|
||||||
|
public static LoanProductRelatedDetail createSettingsForEqualInstallmentAmortizationQuarterly() {
|
||||||
|
MonetaryCurrency currency = new MonetaryCurrencyBuilder().withCode("USD").withDigitsAfterDecimal(2).build();
|
||||||
|
BigDecimal defaultPrincipal = BigDecimal.valueOf(Double.valueOf("200000"));
|
||||||
|
|
||||||
|
// 2% per month, 24% per year
|
||||||
|
BigDecimal defaultNominalInterestRatePerPeriod = BigDecimal.valueOf(Double.valueOf("2"));
|
||||||
|
PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.MONTHS;
|
||||||
|
BigDecimal defaultAnnualNominalInterestRate = BigDecimal.valueOf(Double.valueOf("24"));
|
||||||
|
|
||||||
|
InterestMethod interestMethod = InterestMethod.DECLINING_BALANCE;
|
||||||
|
InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD;
|
||||||
|
Integer repayEvery = Integer.valueOf(3);
|
||||||
|
PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.MONTHS;
|
||||||
|
|
||||||
|
Integer defaultNumberOfRepayments = Integer.valueOf(4);
|
||||||
|
AmortizationMethod amortizationMethod = AmortizationMethod.EQUAL_INSTALLMENTS;
|
||||||
|
|
||||||
|
BigDecimal inArrearsTolerance = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
return new LoanProductRelatedDetail(currency, defaultPrincipal, defaultNominalInterestRatePerPeriod,
|
||||||
|
interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod,
|
||||||
|
interestCalculationPeriodMethod, repayEvery, repaymentFrequencyType, defaultNumberOfRepayments, amortizationMethod, inArrearsTolerance);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user