From 525902cbb2ce7144c5422bd685ab019002b7e995 Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Tue, 7 Aug 2012 14:37:41 +0100 Subject: [PATCH] finish off support for using loan term details of a loan to determine interest due rather than inferring it from the number of repayments and repaid every information. This allows better support for loan schedules where the first repayment is irregular (e.g. not same timescale as repaid every) --- ..._loan_term_fields_and_update_all_loans.sql | 9 +++++ .../mifosng/platform/loan/domain/Loan.java | 26 +++++++++++-- ...CalculateLoanScheduleCommandValidator.java | 30 +++++++++++++-- .../platform/loan/service/LoanAssembler.java | 8 +++- ...ingBalanceMethodLoanScheduleGenerator.java | 38 ++++++++++++------- .../loanschedule/domain/PmtCalculator.java | 2 +- 6 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 mifosng-db/patches/patch-0006a_add_loan_term_fields_and_update_all_loans.sql diff --git a/mifosng-db/patches/patch-0006a_add_loan_term_fields_and_update_all_loans.sql b/mifosng-db/patches/patch-0006a_add_loan_term_fields_and_update_all_loans.sql new file mode 100644 index 000000000..c127577df --- /dev/null +++ b/mifosng-db/patches/patch-0006a_add_loan_term_fields_and_update_all_loans.sql @@ -0,0 +1,9 @@ +ALTER TABLE `portfolio_loan` ADD COLUMN `term_frequency` smallint(5) NOT NULL DEFAULT 0 AFTER `interest_calculated_in_period_enum`; +ALTER TABLE `portfolio_loan` ADD COLUMN `term_period_frequency_enum` smallint(5) NOT NULL DEFAULT 2 AFTER `term_frequency`; + + +UPDATE `mifostenant-default`.`portfolio_loan` +SET +`term_frequency` = `repay_every` * `number_of_repayments`, +`term_period_frequency_enum` = `repayment_period_frequency_enum` +WHERE id > 0; \ No newline at end of file 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 ff495ef0b..0e7eb3420 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 @@ -10,6 +10,8 @@ import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; @@ -60,6 +62,15 @@ public class Loan extends AbstractAuditableCustom { @Embedded private final LoanProductRelatedDetail loanRepaymentScheduleDetail; + + @SuppressWarnings("unused") + @Column(name = "term_frequency", nullable = false) + private Integer termFrequency; + + @SuppressWarnings("unused") + @Enumerated(EnumType.ORDINAL) + @Column(name = "term_period_frequency_enum", nullable = false) + private PeriodFrequencyType termPeriodFrequencyType; @ManyToOne @JoinColumn(name = "loan_status_id", nullable = false) @@ -180,16 +191,23 @@ public class Loan extends AbstractAuditableCustom { return this.loanRepaymentScheduleDetail; } - public void submitApplication(final LocalDate submittedOn, + public void submitApplication( + final Integer loanTermFrequency, + final PeriodFrequencyType loanTermFrequencyType, + final LocalDate submittedOn, final LocalDate expectedDisbursementDate, - LocalDate repaymentsStartingFromDate, - LocalDate interestChargedFromDate, - LoanLifecycleStateMachine lifecycleStateMachine) { + final LocalDate repaymentsStartingFromDate, + final LocalDate interestChargedFromDate, + final LoanLifecycleStateMachine lifecycleStateMachine) { this.loanStatus = lifecycleStateMachine.transition(LoanEvent.LOAN_CREATED, this.loanStatus); + this.termFrequency = loanTermFrequency; + this.termPeriodFrequencyType = loanTermFrequencyType; + this.submittedOnDate = submittedOn.toDateTimeAtCurrentTime().toDate(); + // TODO - this should also match expectedDisbursmentDate + loan term this.expectedMaturityDate = this.repaymentScheduleInstallments .get(this.repaymentScheduleInstallments.size() - 1) .getDueDate().toDateMidnight().toDate(); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculateLoanScheduleCommandValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculateLoanScheduleCommandValidator.java index ec93ace9f..c973ab8cd 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculateLoanScheduleCommandValidator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculateLoanScheduleCommandValidator.java @@ -23,9 +23,33 @@ public class CalculateLoanScheduleCommandValidator { DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); baseDataValidator.reset().parameter("principal").value(command.getPrincipal()).notNull().positiveAmount(); - baseDataValidator.reset().parameter("repaymentFrequencyType").value(command.getRepaymentFrequencyType()).notNull().inMinMaxRange(0, 3); - baseDataValidator.reset().parameter("repaymentEvery").value(command.getRepaymentEvery()).notNull().greaterThanZero(); + + baseDataValidator.reset().parameter("loanTermFrequency").value(command.getLoanTermFrequency()).notNull().greaterThanZero(); + baseDataValidator.reset().parameter("loanTermFrequencyType").value(command.getLoanTermFrequencyType()).notNull().inMinMaxRange(0, 4); + baseDataValidator.reset().parameter("numberOfRepayments").value(command.getNumberOfRepayments()).notNull().greaterThanZero(); + baseDataValidator.reset().parameter("repaymentEvery").value(command.getRepaymentEvery()).notNull().greaterThanZero(); + baseDataValidator.reset().parameter("repaymentFrequencyType").value(command.getRepaymentFrequencyType()).notNull().inMinMaxRange(0, 3); + + // FIXME - this constraint doesnt really need to be here. should be possible to express loan term as say 12 months whilst also saying + // - that the repayment structure is 6 repayments every bi-monthly. + if (!command.getLoanTermFrequencyType().equals(command.getRepaymentFrequencyType())) { + ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.loanTermFrequencyType.not.the.same.as.repaymentFrequencyType", + "The parameters loanTermFrequencyType and repaymentFrequencyType must be the same.", "loanTermFrequencyType", + command.getLoanTermFrequencyType(), command.getRepaymentFrequencyType()); + dataValidationErrors.add(error); + } else { + if (command.getLoanTermFrequency() != null && command.getRepaymentEvery() != null && command.getNumberOfRepayments() != null) { + int suggestsedLoanTerm = command.getRepaymentEvery() * command.getNumberOfRepayments(); + if (command.getLoanTermFrequency().intValue() < suggestsedLoanTerm) { + ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.loanTermFrequency.less.than.repayment.structure.suggests", + "The parameter loanTermFrequency is less than the suggest loan term as indicated by numberOfRepayments and repaymentEvery.", "loanTermFrequency", + command.getLoanTermFrequency(), command.getNumberOfRepayments(), command.getRepaymentEvery()); + dataValidationErrors.add(error); + } + } + } + baseDataValidator.reset().parameter("interestRatePerPeriod").value(command.getInterestRatePerPeriod()).notNull(); baseDataValidator.reset().parameter("interestRateFrequencyType").value(command.getInterestRateFrequencyType()).notNull().inMinMaxRange(0, 3); baseDataValidator.reset().parameter("amortizationType").value(command.getAmortizationType()).notNull().inMinMaxRange(0, 1); @@ -47,7 +71,7 @@ public class CalculateLoanScheduleCommandValidator { if (command.getRepaymentsStartingFromDate() != null && command.getInterestChargedFromDate() == null) { ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.interestCalculatedFromDate.must.be.entered.when.using.repayments.startfrom.field", - "The parameter interestCalculatedFromDate cannot be empty when first repayment date is provided.", "interestCalculatedFromDate", command.getRepaymentsStartingFromDate()); + "The parameter interestCalculatedFromDate cannot be empty when first repayment date is provided.", "interestChargedFromDate", command.getRepaymentsStartingFromDate()); dataValidationErrors.add(error); } else if (command.getRepaymentsStartingFromDate() == null && command.getInterestChargedFromDate() != null) { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java index ea13cabe1..c103d4e17 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java @@ -79,9 +79,12 @@ public class LoanAssembler { 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()); LoanProductRelatedDetail loanRepaymentScheduleDetail = new LoanProductRelatedDetail(currency, @@ -111,7 +114,8 @@ public class LoanAssembler { loan.addRepaymentScheduleInstallment(installment); } - loan.submitApplication(command.getSubmittedOnDate(), command.getExpectedDisbursementDate(), + loan.submitApplication(loanTermFrequency, loanTermFrequencyType, + command.getSubmittedOnDate(), command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate(), command.getInterestChargedFromDate(), defaultLoanLifecycleStateMachine()); 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 5fc386511..0e82ed9b5 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,6 +1,7 @@ package org.mifosng.platform.loanschedule.domain; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; @@ -12,7 +13,6 @@ 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; @@ -43,14 +43,16 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements // 3. determine 'total payment' for each repayment based on pmt function (and hence the total due overall) final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); - final Money totalDuePerInstallment = this.pmtCalculator.calculatePaymentForOnePeriodFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency); - final Money totalDue = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency); + 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 + // 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); - Money totalInterestDue = totalDue.minus(loanScheduleInfo.getPrincipal()); + 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); @@ -66,6 +68,10 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements // 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); @@ -92,15 +98,21 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements principalForInstallment = principalForInstallment.minus(principalDifference.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()); - } + 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); 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 68b84f3af..b3dfad92f 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 @@ -51,7 +51,7 @@ public class PmtCalculator { double interestRateFraction = periodInterestRateForLoanTermPeriod.doubleValue(); double futureValue = 0; - double numberOfPeriods = loanTermFrequency; + double numberOfPeriods = loanTermFrequency.doubleValue(); double principalAmount = principal.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); // work out the total payment (principal + interest components) for each installment due.