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)
This commit is contained in:
@@ -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;
|
||||
@@ -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<AppUser, Long> {
|
||||
|
||||
@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<AppUser, Long> {
|
||||
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();
|
||||
|
||||
+27
-3
@@ -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) {
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
+25
-13
@@ -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);
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user