From af5e2c0e842efe57b500b91ac14d2b6841f34e81 Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Fri, 17 Aug 2012 12:21:02 +0100 Subject: [PATCH] work on fixing interest calculation for declining balance loans. --- .../platform/api/data/CurrencyData.java | 2 +- .../AmortizationLoanScheduleGenerator.java | 19 +++ ...ingBalanceMethodLoanScheduleGenerator.java | 133 ++++-------------- ...entsAmortizationLoanScheduleGenerator.java | 114 +++++++++++++++ ...duleAmortizationLoanScheduleGenerator.java | 133 ++++++++++++++++++ .../PeriodicInterestRateCalculator.java | 8 +- ...alanceMethodLoanScheduleGeneratorTest.java | 112 +++++++++++++++ .../LoanProductRelatedDetailTestHelper.java | 41 ++++++ 8 files changed, 452 insertions(+), 110 deletions(-) create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsWithIrregularRepaymentScheduleAmortizationLoanScheduleGenerator.java create mode 100644 mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGeneratorTest.java create mode 100644 mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/LoanProductRelatedDetailTestHelper.java diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/CurrencyData.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/CurrencyData.java index 9595f864d..338b48482 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/CurrencyData.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/CurrencyData.java @@ -11,7 +11,7 @@ public class CurrencyData implements Serializable { private final String nameCode; 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.name = name; this.decimalPlaces = decimalPlaces; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGenerator.java new file mode 100644 index 000000000..ed45f7a43 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGenerator.java @@ -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 scheduledDates); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java index 0e82ed9b5..ef60498ec 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java @@ -1,7 +1,6 @@ package org.mifosng.platform.loanschedule.domain; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.ArrayList; 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.currency.domain.MonetaryCurrency; 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.PeriodFrequencyType; -public class DecliningBalanceMethodLoanScheduleGenerator implements - LoanScheduleGenerator { +/** + *

+ * Declining balance can amortized (see {@link AmortizationMethod}) in two ways at present: + *

  1. Equal principal payments
  2. Equal installment payments
+ *

+ * + *

+ * When amortized using equal principal payments, the principal component of each installment is fixed and interest due + * is calculated from the outstanding principal balance resulting in a different total payment due for each installment. + *

+ * + *

+ * When amortized using equal installments, the total payment due for each installment is fixed and + * is calculated using the excel like pmt function. The interest due is calculated from the outstanding principal balance + * which results in a principal component that is total payment due minus interest due. + *

+ */ +public class DecliningBalanceMethodLoanScheduleGenerator implements LoanScheduleGenerator { private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); - private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator(); - private final PmtCalculator pmtCalculator = new PmtCalculator(); @Override public LoanSchedule generate( @@ -31,114 +45,21 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements final PeriodFrequencyType loanTermFrequencyType, final LocalDate disbursementDate, final LocalDate firstRepaymentDate, - final LocalDate interestCalculatedFrom, CurrencyData currencyData) { - - List scheduledLoanInstallments = new ArrayList(); + final LocalDate interestCalculatedFrom, + final CurrencyData currencyData) { // 1. generate valid set of 'due dates' based on some of the 'loan attributes' List 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 final BigDecimal periodInterestRateForRepaymentPeriod = this.periodicInterestRateCalculator.calculateFrom(loanScheduleInfo); - // 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 totalDue = 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); - - 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); + // Determine with 'amortisation' approach to use + AmortizationLoanScheduleGenerator generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); + return generator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData, + periodInterestRateForRepaymentPeriod, idealDisbursementDateBasedOnFirstRepaymentDate, scheduledDates); } /** diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java new file mode 100644 index 000000000..7833669c3 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java @@ -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 scheduledDates) { + + List scheduledLoanInstallments = new ArrayList(); + + // 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); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsWithIrregularRepaymentScheduleAmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsWithIrregularRepaymentScheduleAmortizationLoanScheduleGenerator.java new file mode 100644 index 000000000..500e6bfc7 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsWithIrregularRepaymentScheduleAmortizationLoanScheduleGenerator.java @@ -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 scheduledDates) { + + List scheduledLoanInstallments = new ArrayList(); + + // 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); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PeriodicInterestRateCalculator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PeriodicInterestRateCalculator.java index 4b2ced62f..d3b8375f5 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PeriodicInterestRateCalculator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PeriodicInterestRateCalculator.java @@ -34,9 +34,11 @@ public class PeriodicInterestRateCalculator { return loanScheduleInfo.getAnnualNominalInterestRate().divide(divisor, mc).multiply(numberOfPeriods); } - public Money calculateInterestOn(Money outstandingBalance, BigDecimal periodInterestRateForRepaymentPeriod, - int daysInPeriod, - LoanProductRelatedDetail loanScheduleInfo) { + public Money calculateInterestOn( + final Money outstandingBalance, + final BigDecimal periodInterestRateForRepaymentPeriod, + final int daysInPeriod, + final LoanProductRelatedDetail loanScheduleInfo) { MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); Money interestDue = Money.zero(outstandingBalance.getCurrency()); diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGeneratorTest.java b/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGeneratorTest.java new file mode 100644 index 000000000..b8ba3cf15 --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGeneratorTest.java @@ -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 matchesExpectedAmount) { + final Integer installmentIndex = installmentNumber -1; + List 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 matchesExpectedAmount) { + final Integer installmentIndex = installmentNumber -1; + List 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 matchesExpectedAmount) { + final Integer installmentIndex = installmentNumber -1; + List 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; + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/LoanProductRelatedDetailTestHelper.java b/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/LoanProductRelatedDetailTestHelper.java new file mode 100644 index 000000000..b65cec4ba --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/LoanProductRelatedDetailTestHelper.java @@ -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); + } +} \ No newline at end of file