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.Column;
|
||||||
import javax.persistence.Embedded;
|
import javax.persistence.Embedded;
|
||||||
import javax.persistence.Entity;
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.EnumType;
|
||||||
|
import javax.persistence.Enumerated;
|
||||||
import javax.persistence.JoinColumn;
|
import javax.persistence.JoinColumn;
|
||||||
import javax.persistence.ManyToOne;
|
import javax.persistence.ManyToOne;
|
||||||
import javax.persistence.OneToMany;
|
import javax.persistence.OneToMany;
|
||||||
@@ -60,6 +62,15 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
|
|
||||||
@Embedded
|
@Embedded
|
||||||
private final LoanProductRelatedDetail loanRepaymentScheduleDetail;
|
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
|
@ManyToOne
|
||||||
@JoinColumn(name = "loan_status_id", nullable = false)
|
@JoinColumn(name = "loan_status_id", nullable = false)
|
||||||
@@ -180,16 +191,23 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
return this.loanRepaymentScheduleDetail;
|
return this.loanRepaymentScheduleDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void submitApplication(final LocalDate submittedOn,
|
public void submitApplication(
|
||||||
|
final Integer loanTermFrequency,
|
||||||
|
final PeriodFrequencyType loanTermFrequencyType,
|
||||||
|
final LocalDate submittedOn,
|
||||||
final LocalDate expectedDisbursementDate,
|
final LocalDate expectedDisbursementDate,
|
||||||
LocalDate repaymentsStartingFromDate,
|
final LocalDate repaymentsStartingFromDate,
|
||||||
LocalDate interestChargedFromDate,
|
final LocalDate interestChargedFromDate,
|
||||||
LoanLifecycleStateMachine lifecycleStateMachine) {
|
final LoanLifecycleStateMachine lifecycleStateMachine) {
|
||||||
|
|
||||||
this.loanStatus = lifecycleStateMachine.transition(LoanEvent.LOAN_CREATED, this.loanStatus);
|
this.loanStatus = lifecycleStateMachine.transition(LoanEvent.LOAN_CREATED, this.loanStatus);
|
||||||
|
|
||||||
|
this.termFrequency = loanTermFrequency;
|
||||||
|
this.termPeriodFrequencyType = loanTermFrequencyType;
|
||||||
|
|
||||||
this.submittedOnDate = submittedOn.toDateTimeAtCurrentTime().toDate();
|
this.submittedOnDate = submittedOn.toDateTimeAtCurrentTime().toDate();
|
||||||
|
|
||||||
|
// TODO - this should also match expectedDisbursmentDate + loan term
|
||||||
this.expectedMaturityDate = this.repaymentScheduleInstallments
|
this.expectedMaturityDate = this.repaymentScheduleInstallments
|
||||||
.get(this.repaymentScheduleInstallments.size() - 1)
|
.get(this.repaymentScheduleInstallments.size() - 1)
|
||||||
.getDueDate().toDateMidnight().toDate();
|
.getDueDate().toDateMidnight().toDate();
|
||||||
|
|||||||
+27
-3
@@ -23,9 +23,33 @@ public class CalculateLoanScheduleCommandValidator {
|
|||||||
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
|
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
|
||||||
|
|
||||||
baseDataValidator.reset().parameter("principal").value(command.getPrincipal()).notNull().positiveAmount();
|
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("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("interestRatePerPeriod").value(command.getInterestRatePerPeriod()).notNull();
|
||||||
baseDataValidator.reset().parameter("interestRateFrequencyType").value(command.getInterestRateFrequencyType()).notNull().inMinMaxRange(0, 3);
|
baseDataValidator.reset().parameter("interestRateFrequencyType").value(command.getInterestRateFrequencyType()).notNull().inMinMaxRange(0, 3);
|
||||||
baseDataValidator.reset().parameter("amortizationType").value(command.getAmortizationType()).notNull().inMinMaxRange(0, 1);
|
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) {
|
if (command.getRepaymentsStartingFromDate() != null && command.getInterestChargedFromDate() == null) {
|
||||||
|
|
||||||
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.interestCalculatedFromDate.must.be.entered.when.using.repayments.startfrom.field",
|
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);
|
dataValidationErrors.add(error);
|
||||||
} else if (command.getRepaymentsStartingFromDate() == null && command.getInterestChargedFromDate() != null) {
|
} else if (command.getRepaymentsStartingFromDate() == null && command.getInterestChargedFromDate() != null) {
|
||||||
|
|
||||||
|
|||||||
@@ -79,9 +79,12 @@ public class LoanAssembler {
|
|||||||
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType());
|
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType());
|
||||||
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command.getInterestCalculationPeriodType());
|
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 Integer repayEvery = command.getRepaymentEvery();
|
||||||
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequencyType());
|
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequencyType());
|
||||||
final Integer defaultNumberOfInstallments = command.getNumberOfRepayments();
|
|
||||||
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationType());
|
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationType());
|
||||||
|
|
||||||
LoanProductRelatedDetail loanRepaymentScheduleDetail = new LoanProductRelatedDetail(currency,
|
LoanProductRelatedDetail loanRepaymentScheduleDetail = new LoanProductRelatedDetail(currency,
|
||||||
@@ -111,7 +114,8 @@ public class LoanAssembler {
|
|||||||
loan.addRepaymentScheduleInstallment(installment);
|
loan.addRepaymentScheduleInstallment(installment);
|
||||||
}
|
}
|
||||||
|
|
||||||
loan.submitApplication(command.getSubmittedOnDate(), command.getExpectedDisbursementDate(),
|
loan.submitApplication(loanTermFrequency, loanTermFrequencyType,
|
||||||
|
command.getSubmittedOnDate(), command.getExpectedDisbursementDate(),
|
||||||
command.getRepaymentsStartingFromDate(), command.getInterestChargedFromDate(),
|
command.getRepaymentsStartingFromDate(), command.getInterestChargedFromDate(),
|
||||||
defaultLoanLifecycleStateMachine());
|
defaultLoanLifecycleStateMachine());
|
||||||
|
|
||||||
|
|||||||
+25
-13
@@ -1,6 +1,7 @@
|
|||||||
package org.mifosng.platform.loanschedule.domain;
|
package org.mifosng.platform.loanschedule.domain;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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.api.data.ScheduledLoanInstallment;
|
||||||
import org.mifosng.platform.currency.domain.MonetaryCurrency;
|
import org.mifosng.platform.currency.domain.MonetaryCurrency;
|
||||||
import org.mifosng.platform.currency.domain.Money;
|
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.LoanProductRelatedDetail;
|
||||||
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
|
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)
|
// 3. determine 'total payment' for each repayment based on pmt function (and hence the total due overall)
|
||||||
final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency();
|
final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency();
|
||||||
final Money totalDuePerInstallment = this.pmtCalculator.calculatePaymentForOnePeriodFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
|
Money totalDuePerInstallment = this.pmtCalculator.calculatePaymentForOnePeriodFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
|
||||||
final Money totalDue = this.pmtCalculator.calculateTotalRepaymentFrom(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 BigDecimal periodInterestRateForLoanTermPeriod = this.periodicInterestRateCalculator.calculateFrom(loanTermFrequencyType, loanScheduleInfo.getAnnualNominalInterestRate());
|
||||||
final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo.getPrincipal(), loanTermFrequency, periodInterestRateForLoanTermPeriod, monetaryCurrency);
|
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 outstandingBalance = loanScheduleInfo.getPrincipal();
|
||||||
Money totalPrincipal = Money.zero(monetaryCurrency);
|
Money totalPrincipal = Money.zero(monetaryCurrency);
|
||||||
Money totalInterest = Money.zero(monetaryCurrency);
|
Money totalInterest = Money.zero(monetaryCurrency);
|
||||||
@@ -66,6 +68,10 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements
|
|||||||
// number of days from startDate to this scheduledDate
|
// number of days from startDate to this scheduledDate
|
||||||
int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()).getDays();
|
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 interestForInstallment = this.periodicInterestRateCalculator.calculateInterestOn(outstandingBalance, periodInterestRateForRepaymentPeriod, daysInPeriod, loanScheduleInfo);
|
||||||
Money principalForInstallment = this.periodicInterestRateCalculator.calculatePrincipalOn(totalDuePerInstallment, interestForInstallment, loanScheduleInfo);
|
Money principalForInstallment = this.periodicInterestRateCalculator.calculatePrincipalOn(totalDuePerInstallment, interestForInstallment, loanScheduleInfo);
|
||||||
|
|
||||||
@@ -92,15 +98,21 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements
|
|||||||
principalForInstallment = principalForInstallment.minus(principalDifference.abs());
|
principalForInstallment = principalForInstallment.minus(principalDifference.abs());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loanScheduleInfo.getAmortizationMethod().equals(
|
final Money interestDifference = totalInterest.minus(totalInterestDue);
|
||||||
AmortizationMethod.EQUAL_INSTALLMENTS)) {
|
if (interestDifference.isLessThanZero()) {
|
||||||
Money interestDifference = totalInterest.minus(totalInterestDue);
|
interestForInstallment = interestForInstallment.plus(interestDifference.abs());
|
||||||
if (interestDifference.isLessThanZero()) {
|
} else if (interestDifference.isGreaterThanZero()) {
|
||||||
interestForInstallment = interestForInstallment.plus(interestDifference.abs());
|
interestForInstallment = interestForInstallment.minus(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);
|
Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment);
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ public class PmtCalculator {
|
|||||||
double interestRateFraction = periodInterestRateForLoanTermPeriod.doubleValue();
|
double interestRateFraction = periodInterestRateForLoanTermPeriod.doubleValue();
|
||||||
|
|
||||||
double futureValue = 0;
|
double futureValue = 0;
|
||||||
double numberOfPeriods = loanTermFrequency;
|
double numberOfPeriods = loanTermFrequency.doubleValue();
|
||||||
double principalAmount = principal.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue();
|
double principalAmount = principal.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue();
|
||||||
|
|
||||||
// work out the total payment (principal + interest components) for each installment due.
|
// work out the total payment (principal + interest components) for each installment due.
|
||||||
|
|||||||
Reference in New Issue
Block a user