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.