From 068620c8104c93042090c5183beca75708af00e4 Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Fri, 17 Aug 2012 14:04:38 +0100 Subject: [PATCH] test amortisation for declining balance and support irregular first repayment on flat loans. --- ...rtizationLoanScheduleGeneratorFactory.java | 26 +++ ...ingBalanceMethodLoanScheduleGenerator.java | 10 +- .../DefaultLoanScheduleGeneratorFactory.java | 2 +- ...ipalAmortizationLoanScheduleGenerator.java | 102 +++++++++++ ...a => FlatMethodLoanScheduleGenerator.java} | 14 +- ...alanceMethodLoanScheduleGeneratorTest.java | 30 +++- .../FlatMethodLoanScheduleGeneratorTest.java | 164 ++++++++++++++++++ .../LoanProductRelatedDetailTestHelper.java | 104 +++++++++++ 8 files changed, 430 insertions(+), 22 deletions(-) create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java rename mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/{FlatLoanScheduleGenerator.java => FlatMethodLoanScheduleGenerator.java} (87%) create mode 100644 mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGeneratorTest.java diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java new file mode 100644 index 000000000..c47e2a977 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java @@ -0,0 +1,26 @@ +package org.mifosng.platform.loanschedule.domain; + +import org.mifosng.platform.loan.domain.AmortizationMethod; + +public class AmortizationLoanScheduleGeneratorFactory { + + public AmortizationLoanScheduleGenerator createGenerator(final AmortizationMethod amortizationMethod) { + + AmortizationLoanScheduleGenerator generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); + + switch (amortizationMethod) { + case INVALID: + generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); + break; + case EQUAL_INSTALLMENTS: + generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); + break; + case EQUAL_PRINCIPAL: + generator = new EqualPrincipalAmortizationLoanScheduleGenerator(); + break; + } + + return generator; + } + +} 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 ef60498ec..a336c9f0d 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,17 +1,11 @@ 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.AmortizationMethod; import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; import org.mifosng.platform.loan.domain.PeriodFrequencyType; @@ -37,6 +31,7 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements LoanSchedule private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator(); + private final AmortizationLoanScheduleGeneratorFactory amortizationLoanScheduleGeneratorFactory = new AmortizationLoanScheduleGeneratorFactory(); @Override public LoanSchedule generate( @@ -57,7 +52,8 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements LoanSchedule final BigDecimal periodInterestRateForRepaymentPeriod = this.periodicInterestRateCalculator.calculateFrom(loanScheduleInfo); // Determine with 'amortisation' approach to use - AmortizationLoanScheduleGenerator generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); + AmortizationLoanScheduleGenerator generator = this.amortizationLoanScheduleGeneratorFactory.createGenerator(loanScheduleInfo.getAmortizationMethod()); + return generator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData, periodInterestRateForRepaymentPeriod, idealDisbursementDateBasedOnFirstRepaymentDate, scheduledDates); } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java index f172ea68f..55b54c6c8 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java @@ -12,7 +12,7 @@ public class DefaultLoanScheduleGeneratorFactory implements switch (interestMethod) { case FLAT: - loanScheduleGenerator = new FlatLoanScheduleGenerator(); + loanScheduleGenerator = new FlatMethodLoanScheduleGenerator(); break; case DECLINING_BALANCE: loanScheduleGenerator = new DecliningBalanceMethodLoanScheduleGenerator(); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java new file mode 100644 index 000000000..fc2a32c0f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java @@ -0,0 +1,102 @@ +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 EqualPrincipalAmortizationLoanScheduleGenerator implements AmortizationLoanScheduleGenerator { + + private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator(); + private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); + + @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(); + + // 1. we know 'principal' must be equal over repayments so use this to calculate interest and thus total repayment due. + final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); + Money totalDuePerInstallment = Money.zero(monetaryCurrency); + + 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); + 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); + 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()); + } + } + + 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/FlatLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGenerator.java similarity index 87% rename from mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatLoanScheduleGenerator.java rename to mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGenerator.java index b2650dd1b..a87f1155c 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGenerator.java @@ -15,7 +15,7 @@ import org.mifosng.platform.currency.domain.Money; import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; import org.mifosng.platform.loan.domain.PeriodFrequencyType; -public class FlatLoanScheduleGenerator implements LoanScheduleGenerator { +public class FlatMethodLoanScheduleGenerator implements LoanScheduleGenerator { private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); @@ -49,18 +49,6 @@ public class FlatLoanScheduleGenerator implements LoanScheduleGenerator { .dividedBy(loanScheduleInfo.getNumberOfRepayments(), RoundingMode.HALF_EVEN); -// BigDecimal paymentPeriodsInYear = BigDecimal -// .valueOf(this.paymentPeriodsInOneYearCalculator -// .calculate(loanScheduleInfo.getRepaymentPeriodFrequencyType())); - -// BigDecimal periodicInterestRate = loanScheduleInfo -// .getAnnualNominalInterestRate() -// .divide(paymentPeriodsInYear, mc) -// .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc) -// .multiply(BigDecimal.valueOf(loanScheduleInfo.getRepayEvery())); - -// Money interestPerInstallment = loanScheduleInfo.getPrincipal().multiplyRetainScale(periodicInterestRate, RoundingMode.HALF_EVEN); - Money outstandingBalance = loanScheduleInfo.getPrincipal(); Money totalPrincipal = Money.zero(outstandingBalance.getCurrency()); Money totalInterest = 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 index b8ba3cf15..8369aeb6a 100644 --- 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 @@ -59,7 +59,35 @@ public class DecliningBalanceMethodLoanScheduleGeneratorTest { 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")); + given(schedule).assertTotalPaymentDueOf(4, is("57718.29")).assertInterestDueComponentOf(4, is("3267.07")).assertPrincipalDueComponentOf(4, is("54451.22")); + } + + @Test + public void givenEqualPrincipalAmortisationShouldCalculateInterestComponentFromOutstandingPrincipalBlanceResultingInDecliningInterestAndTotalRepayments() { + + // setup + Integer loanTermFrequency = 12; + PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.MONTHS; + loanScheduleInfo = LoanProductRelatedDetailTestHelper.createSettingsForEqualPrincipalAmortizationQuarterly(); + + 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).assertPrincipalDueComponentOf(1, is("50000.00")).assertInterestDueComponentOf(1, is("12000.00")).assertTotalPaymentDueOf(1, is("62000.00")); + given(schedule).assertPrincipalDueComponentOf(2, is("50000.00")).assertInterestDueComponentOf(2, is("9000.00")).assertTotalPaymentDueOf(2, is("59000.00")); + given(schedule).assertPrincipalDueComponentOf(3, is("50000.00")).assertInterestDueComponentOf(3, is("6000.00")).assertTotalPaymentDueOf(3, is("56000.00")); + given(schedule).assertPrincipalDueComponentOf(4, is("50000.00")).assertInterestDueComponentOf(4, is("3000.00")).assertTotalPaymentDueOf(4, is("53000.00")); } private LoanScheduleAssertionBuilder given(final LoanSchedule schedule) { diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGeneratorTest.java b/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGeneratorTest.java new file mode 100644 index 000000000..ff6161bd7 --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGeneratorTest.java @@ -0,0 +1,164 @@ +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.AmortizationMethod; +import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class FlatMethodLoanScheduleGeneratorTest { + + // class under test + private FlatMethodLoanScheduleGenerator flatLoanScheduleGenerator; + + // test doubles + private CurrencyData currencyData = null; + private LoanProductRelatedDetail loanScheduleInfo = null; + + @Before + public void setupForEachTest() { + currencyData = new CurrencyData("USD", "US Dollar", 2, "$", "currency.USD"); + + flatLoanScheduleGenerator = new FlatMethodLoanScheduleGenerator(); + } + + @Test + public void givenFlatMethodShouldCalculateFlatInterestComponentToDerivePrincipalAndTotalRepaymentRegardlessOfEqualInstallmentAmortisation() { + + // setup + Integer loanTermFrequency = 12; + PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.MONTHS; + loanScheduleInfo = LoanProductRelatedDetailTestHelper.createSettingsForFlatQuarterly(AmortizationMethod.EQUAL_INSTALLMENTS); + + 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 = flatLoanScheduleGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType, + disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData); + + // verification (post assertions) + given(schedule).assertHasInstallmentSizeOf(4); + given(schedule).assertPrincipalDueComponentOf(1, is("50000.00")).assertInterestDueComponentOf(1, is("12000.00")).assertTotalPaymentDueOf(1, is("62000.00")); + given(schedule).assertPrincipalDueComponentOf(2, is("50000.00")).assertInterestDueComponentOf(2, is("12000.00")).assertTotalPaymentDueOf(2, is("62000.00")); + given(schedule).assertPrincipalDueComponentOf(3, is("50000.00")).assertInterestDueComponentOf(3, is("12000.00")).assertTotalPaymentDueOf(3, is("62000.00")); + given(schedule).assertPrincipalDueComponentOf(4, is("50000.00")).assertInterestDueComponentOf(4, is("12000.00")).assertTotalPaymentDueOf(4, is("62000.00")); + } + + @Test + public void givenFlatMethodShouldCalculateFlatInterestComponentToDerivePrincipalAndTotalRepaymentRegardlessOfEqualPrincipalAmortisation() { + + // setup + Integer loanTermFrequency = 12; + PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.MONTHS; + loanScheduleInfo = LoanProductRelatedDetailTestHelper.createSettingsForFlatQuarterly(AmortizationMethod.EQUAL_PRINCIPAL); + + 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 = flatLoanScheduleGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType, + disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData); + + // verification (post assertions) + given(schedule).assertHasInstallmentSizeOf(4); + given(schedule).assertPrincipalDueComponentOf(1, is("50000.00")).assertInterestDueComponentOf(1, is("12000.00")).assertTotalPaymentDueOf(1, is("62000.00")); + given(schedule).assertPrincipalDueComponentOf(2, is("50000.00")).assertInterestDueComponentOf(2, is("12000.00")).assertTotalPaymentDueOf(2, is("62000.00")); + given(schedule).assertPrincipalDueComponentOf(3, is("50000.00")).assertInterestDueComponentOf(3, is("12000.00")).assertTotalPaymentDueOf(3, is("62000.00")); + given(schedule).assertPrincipalDueComponentOf(4, is("50000.00")).assertInterestDueComponentOf(4, is("12000.00")).assertTotalPaymentDueOf(4, is("62000.00")); + } + + @Test + public void givenFlatMethodAndIrregularFirstRepaymentShouldCalculateFlatInterestComponent() { + + // setup + currencyData = new CurrencyData("KSH", "Keyan Shilling", 0, "KSh", "currency.KSH"); + Integer loanTermFrequency = 9; + PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.MONTHS; + loanScheduleInfo = LoanProductRelatedDetailTestHelper.createSettingsForIrregularFlatEveryFourMonths(); + + LocalDate disbursementDate = new LocalDate(2012, 3, 27); + LocalDate firstRepaymentDate = new LocalDate(2011, 7, 27); + LocalDate interestCalculatedFrom = disbursementDate; + + // exercise test + LoanSchedule schedule = flatLoanScheduleGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType, + disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData); + + // verification (post assertions) + given(schedule).assertHasInstallmentSizeOf(2); + given(schedule).assertPrincipalDueComponentOf(1, is("7500")).assertInterestDueComponentOf(1, is("1350")).assertTotalPaymentDueOf(1, is("8850")); + given(schedule).assertPrincipalDueComponentOf(2, is("7500")).assertInterestDueComponentOf(2, is("1350")).assertTotalPaymentDueOf(2, is("8850")); + } + + 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 index b65cec4ba..30713f0c8 100644 --- 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 @@ -14,6 +14,34 @@ 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 createSettingsForEqualPrincipalAmortizationQuarterly() { + + 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_PRINCIPAL; + + BigDecimal inArrearsTolerance = BigDecimal.ZERO; + + return createLoanProductRelatedDetail(currency, defaultPrincipal, + defaultNominalInterestRatePerPeriod, + interestPeriodFrequencyType, defaultAnnualNominalInterestRate, + interestMethod, interestCalculationPeriodMethod, repayEvery, + repaymentFrequencyType, defaultNumberOfRepayments, + amortizationMethod, inArrearsTolerance); + } public static LoanProductRelatedDetail createSettingsForEqualInstallmentAmortizationQuarterly() { MonetaryCurrency currency = new MonetaryCurrencyBuilder().withCode("USD").withDigitsAfterDecimal(2).build(); @@ -34,6 +62,82 @@ public class LoanProductRelatedDetailTestHelper { BigDecimal inArrearsTolerance = BigDecimal.ZERO; + return createLoanProductRelatedDetail(currency, defaultPrincipal, + defaultNominalInterestRatePerPeriod, + interestPeriodFrequencyType, defaultAnnualNominalInterestRate, + interestMethod, interestCalculationPeriodMethod, repayEvery, + repaymentFrequencyType, defaultNumberOfRepayments, + amortizationMethod, inArrearsTolerance); + } + + public static LoanProductRelatedDetail createSettingsForFlatQuarterly(final AmortizationMethod amortizationMethod) { + + 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.FLAT; + InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD; + Integer repayEvery = Integer.valueOf(3); + PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.MONTHS; + + Integer defaultNumberOfRepayments = Integer.valueOf(4); + + BigDecimal inArrearsTolerance = BigDecimal.ZERO; + + return createLoanProductRelatedDetail(currency, defaultPrincipal, + defaultNominalInterestRatePerPeriod, + interestPeriodFrequencyType, defaultAnnualNominalInterestRate, + interestMethod, interestCalculationPeriodMethod, repayEvery, + repaymentFrequencyType, defaultNumberOfRepayments, + amortizationMethod, inArrearsTolerance); + } + + public static LoanProductRelatedDetail createSettingsForIrregularFlatEveryFourMonths() { + + MonetaryCurrency currency = new MonetaryCurrencyBuilder().withCode("KSH").withDigitsAfterDecimal(0).build(); + BigDecimal defaultPrincipal = BigDecimal.valueOf(Double.valueOf("15000")); + + // 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.FLAT; + InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD; + Integer repayEvery = Integer.valueOf(3); + PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.MONTHS; + + Integer defaultNumberOfRepayments = Integer.valueOf(2); + + BigDecimal inArrearsTolerance = BigDecimal.ZERO; + + AmortizationMethod amortizationMethod = AmortizationMethod.EQUAL_PRINCIPAL; + + return createLoanProductRelatedDetail(currency, defaultPrincipal, + defaultNominalInterestRatePerPeriod, + interestPeriodFrequencyType, defaultAnnualNominalInterestRate, + interestMethod, interestCalculationPeriodMethod, repayEvery, + repaymentFrequencyType, defaultNumberOfRepayments, + amortizationMethod, inArrearsTolerance); + } + + + private static LoanProductRelatedDetail createLoanProductRelatedDetail( + MonetaryCurrency currency, BigDecimal defaultPrincipal, + BigDecimal defaultNominalInterestRatePerPeriod, + PeriodFrequencyType interestPeriodFrequencyType, + BigDecimal defaultAnnualNominalInterestRate, + InterestMethod interestMethod, + InterestCalculationPeriodMethod interestCalculationPeriodMethod, + Integer repayEvery, PeriodFrequencyType repaymentFrequencyType, + Integer defaultNumberOfRepayments, + AmortizationMethod amortizationMethod, BigDecimal inArrearsTolerance) { + return new LoanProductRelatedDetail(currency, defaultPrincipal, defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, repayEvery, repaymentFrequencyType, defaultNumberOfRepayments, amortizationMethod, inArrearsTolerance);