From 5638a0880c0a33dc5397878259b4ec256aae672d Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Tue, 7 Aug 2012 10:44:25 +0100 Subject: [PATCH] calculate total interest based on etered loan term. --- .../platform/api/LoansApiResource.java | 9 ++-- .../mifosng/platform/loan/domain/Loan.java | 3 +- .../CalculationPlatformServiceImpl.java | 9 +++- ...ingBalanceMethodLoanScheduleGenerator.java | 7 +++ .../domain/FlatLoanScheduleGenerator.java | 44 ++++++++++++++----- .../domain/LoanScheduleGenerator.java | 3 ++ .../PeriodicInterestRateCalculator.java | 13 +++++- .../loanschedule/domain/PmtCalculator.java | 29 +++++++++++- 8 files changed, 92 insertions(+), 25 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java index 7242a60d3..d4d6d4b7b 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java @@ -147,10 +147,8 @@ public class LoansApiResource { SubmitLoanApplicationCommand command = this.apiDataConversionService .convertJsonToSubmitLoanApplicationCommand(jsonRequestBody); - CalculateLoanScheduleCommand calculateLoanScheduleCommand = command - .toCalculateLoanScheduleCommand(); - LoanSchedule loanSchedule = this.calculationPlatformService - .calculateLoanSchedule(calculateLoanScheduleCommand); + CalculateLoanScheduleCommand calculateLoanScheduleCommand = command.toCalculateLoanScheduleCommand(); + LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(calculateLoanScheduleCommand); // for now just auto generating the loan schedule and setting support // for 'manual' loan schedule creation later. @@ -160,8 +158,7 @@ public class LoansApiResource { return Response.ok().entity(loanSchedule).build(); } - EntityIdentifier identifier = this.loanWritePlatformService - .submitLoanApplication(command); + EntityIdentifier identifier = this.loanWritePlatformService.submitLoanApplication(command); return Response.ok().entity(identifier).build(); } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java index 8a7fa34e4..ff495ef0b 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java @@ -186,8 +186,7 @@ public class Loan extends AbstractAuditableCustom { LocalDate interestChargedFromDate, LoanLifecycleStateMachine lifecycleStateMachine) { - this.loanStatus = lifecycleStateMachine.transition( - LoanEvent.LOAN_CREATED, this.loanStatus); + this.loanStatus = lifecycleStateMachine.transition(LoanEvent.LOAN_CREATED, this.loanStatus); this.submittedOnDate = submittedOn.toDateTimeAtCurrentTime().toDate(); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformServiceImpl.java index 717cc34dd..4f316d081 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformServiceImpl.java @@ -67,9 +67,14 @@ public class CalculationPlatformServiceImpl implements final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyType()); final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType()); final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command.getInterestCalculationPeriodType()); + + final Integer loanTermFrequency = command.getLoanTermFrequency(); + final PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.fromInt(command.getLoanTermFrequencyType()); + + final Integer defaultNumberOfInstallments = command.getNumberOfRepayments(); final Integer repayEvery = command.getRepaymentEvery(); final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequencyType()); - final Integer defaultNumberOfInstallments = command.getNumberOfRepayments(); + final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationType()); final BigDecimal defaultAnnualNominalInterestRate = this.aprCalculator.calculateFrom(interestPeriodFrequencyType, defaultNominalInterestRatePerPeriod); @@ -96,7 +101,7 @@ public class CalculationPlatformServiceImpl implements CurrencyData currencyData = new CurrencyData(applicationCurrency.getCode(), applicationCurrency.getName(), currency.getDigitsAfterDecimal(), applicationCurrency.getDisplaySymbol(), applicationCurrency.getNameCode()); - return loanScheduleGenerator.generate(loanScheduleInfo, command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate(), + return loanScheduleGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType, command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate(), command.getInterestChargedFromDate(), currencyData); } 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 f1cf01dbf..5fc386511 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 @@ -14,6 +14,7 @@ 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 { @@ -26,6 +27,8 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements @Override public LoanSchedule generate( final LoanProductRelatedDetail loanScheduleInfo, + final Integer loanTermFrequency, + final PeriodFrequencyType loanTermFrequencyType, final LocalDate disbursementDate, final LocalDate firstRepaymentDate, final LocalDate interestCalculatedFrom, CurrencyData currencyData) { @@ -43,6 +46,10 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements final 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 + final BigDecimal periodInterestRateForLoanTermPeriod = this.periodicInterestRateCalculator.calculateFrom(loanTermFrequencyType, loanScheduleInfo.getAnnualNominalInterestRate()); + final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo.getPrincipal(), loanTermFrequency, periodInterestRateForLoanTermPeriod, monetaryCurrency); + Money totalInterestDue = totalDue.minus(loanScheduleInfo.getPrincipal()); Money outstandingBalance = loanScheduleInfo.getPrincipal(); Money totalPrincipal = Money.zero(monetaryCurrency); 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/FlatLoanScheduleGenerator.java index d8d014ce0..b2650dd1b 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/FlatLoanScheduleGenerator.java @@ -13,6 +13,7 @@ import org.mifosng.platform.api.data.MoneyData; import org.mifosng.platform.api.data.ScheduledLoanInstallment; 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 { @@ -22,6 +23,8 @@ public class FlatLoanScheduleGenerator implements LoanScheduleGenerator { @Override public LoanSchedule generate( final LoanProductRelatedDetail loanScheduleInfo, + final Integer loanTermFrequency, + final PeriodFrequencyType loanTermFrequencyType, final LocalDate disbursementDate, final LocalDate firstRepaymentDate, final LocalDate interestCalculatedFrom, final CurrencyData currencyData) { @@ -31,40 +34,57 @@ public class FlatLoanScheduleGenerator implements LoanScheduleGenerator { MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); - // FIXME - use the expected loan term to work out interest due - // loan term = 9 months + BigDecimal loanTermPeriodsInYear = BigDecimal.valueOf(this.paymentPeriodsInOneYearCalculator.calculate(loanTermFrequencyType)); + BigDecimal interestRateForLoanTerm = loanScheduleInfo + .getAnnualNominalInterestRate() + .divide(loanTermPeriodsInYear, mc) + .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc) + .multiply(BigDecimal.valueOf(loanTermFrequency)); + + Money totalInterestForLoanTerm = loanScheduleInfo.getPrincipal().multiplyRetainScale(interestRateForLoanTerm, RoundingMode.HALF_EVEN); + Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(Long.valueOf(loanScheduleInfo.getNumberOfRepayments()), RoundingMode.HALF_EVEN); + Money principalPerInstallment = loanScheduleInfo.getPrincipal() .dividedBy(loanScheduleInfo.getNumberOfRepayments(), RoundingMode.HALF_EVEN); - BigDecimal paymentPeriodsInYear = BigDecimal - .valueOf(this.paymentPeriodsInOneYearCalculator - .calculate(loanScheduleInfo.getRepaymentPeriodFrequencyType())); +// 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())); +// 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 interestPerInstallment = loanScheduleInfo.getPrincipal().multiplyRetainScale(periodicInterestRate, RoundingMode.HALF_EVEN); Money outstandingBalance = loanScheduleInfo.getPrincipal(); Money totalPrincipal = Money.zero(outstandingBalance.getCurrency()); + Money totalInterest = Money.zero(outstandingBalance.getCurrency()); LocalDate startDate = disbursementDate; int installmentNumber = 1; for (LocalDate scheduledDueDate : scheduledDates) { totalPrincipal = totalPrincipal.plus(principalPerInstallment); + totalInterest = totalInterest.plus(interestPerInstallment); if (installmentNumber == loanScheduleInfo.getNumberOfRepayments()) { - Money difference = totalPrincipal.minus(loanScheduleInfo.getPrincipal()); + final Money difference = totalPrincipal.minus(loanScheduleInfo.getPrincipal()); if (difference.isLessThanZero()) { principalPerInstallment = principalPerInstallment.plus(difference.abs()); } else if (difference.isGreaterThanZero()) { principalPerInstallment = principalPerInstallment.minus(difference.abs()); } + + final Money interestDifference = totalInterest.minus(totalInterestForLoanTerm); + if (interestDifference.isLessThanZero()) { + interestPerInstallment = interestPerInstallment.plus(interestDifference.abs()); + } else if (interestDifference.isGreaterThanZero()) { + interestPerInstallment = interestPerInstallment.minus(interestDifference.abs()); + } } Money totalInstallmentDue = principalPerInstallment.plus(interestPerInstallment); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java index 1846d61fa..6259caa9c 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java @@ -4,10 +4,13 @@ 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; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; public interface LoanScheduleGenerator { LoanSchedule generate(LoanProductRelatedDetail loanScheduleInfo, + Integer loanTermFrequency, + PeriodFrequencyType loanTermFrequencyType, LocalDate disbursementDate, LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom, CurrencyData currencyData); 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 8c8f9c827..4b2ced62f 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 @@ -6,12 +6,23 @@ import java.math.RoundingMode; import org.mifosng.platform.currency.domain.Money; import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; public class PeriodicInterestRateCalculator { private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); - public BigDecimal calculateFrom(LoanProductRelatedDetail loanScheduleInfo) { + public BigDecimal calculateFrom(final PeriodFrequencyType loanTermFrequencyType, final BigDecimal annualNominalInterestRate) { + + MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); + + Integer paymentPeriodsInOneYear = this.paymentPeriodsInOneYearCalculator.calculate(loanTermFrequencyType); + + BigDecimal divisor = BigDecimal.valueOf(paymentPeriodsInOneYear * 100); + return annualNominalInterestRate.divide(divisor, mc); + } + + public BigDecimal calculateFrom(final LoanProductRelatedDetail loanScheduleInfo) { MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PmtCalculator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PmtCalculator.java index a2aa47976..68b84f3af 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PmtCalculator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PmtCalculator.java @@ -8,7 +8,9 @@ import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; public class PmtCalculator { - public Money calculatePaymentForOnePeriodFrom(LoanProductRelatedDetail loanScheduleInfo, BigDecimal periodInterestRateForRepaymentPeriod, MonetaryCurrency monetaryCurrency) { + public Money calculatePaymentForOnePeriodFrom(final LoanProductRelatedDetail loanScheduleInfo, + final BigDecimal periodInterestRateForRepaymentPeriod, + final MonetaryCurrency monetaryCurrency) { double interestRateFraction = periodInterestRateForRepaymentPeriod.doubleValue(); double futureValue = 0; @@ -22,7 +24,9 @@ public class PmtCalculator { return Money.of(monetaryCurrency, BigDecimal.valueOf(paymentPerInstallment)); } - public Money calculateTotalRepaymentFrom(LoanProductRelatedDetail loanScheduleInfo, BigDecimal periodInterestRateForRepaymentPeriod, MonetaryCurrency monetaryCurrency) { + public Money calculateTotalRepaymentFrom(final LoanProductRelatedDetail loanScheduleInfo, + final BigDecimal periodInterestRateForRepaymentPeriod, + final MonetaryCurrency monetaryCurrency) { double interestRateFraction = periodInterestRateForRepaymentPeriod.doubleValue(); double futureValue = 0; @@ -37,6 +41,27 @@ public class PmtCalculator { return Money.of(monetaryCurrency, BigDecimal.valueOf(totalRepayment)); } + + public Money calculateTotalRepaymentFrom( + final Money principal, + final Integer loanTermFrequency, + final BigDecimal periodInterestRateForLoanTermPeriod, + final MonetaryCurrency monetaryCurrency) { + + double interestRateFraction = periodInterestRateForLoanTermPeriod.doubleValue(); + + double futureValue = 0; + double numberOfPeriods = loanTermFrequency; + double principalAmount = principal.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); + + // work out the total payment (principal + interest components) for each installment due. + // use the period type and interest as expressed e.g. 2% per month or 24% per year even is 'daily' interest calculation is selected + double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principalAmount, futureValue, false); + + double totalRepayment = paymentPerInstallment * loanTermFrequency; + + return Money.of(monetaryCurrency, BigDecimal.valueOf(totalRepayment)); + } /** * PMT calculates a fixed monthly payment to be paid by borrower every 'period' to ensure loan is paid off in full (with interest).