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:
+ *
- Equal principal payments
- 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