FINERACT-1968: Progressive Loan schedule handling - interest handling (FLAT interest, Same as repayment period, Equal installments)
This commit is contained in:
+22
-6
@@ -290,10 +290,13 @@ public final class LoanApplicationTerms {
|
|||||||
this.buyDownFeeCalculationType = builder.buyDownFeeCalculationType;
|
this.buyDownFeeCalculationType = builder.buyDownFeeCalculationType;
|
||||||
this.buyDownFeeStrategy = builder.buyDownFeeStrategy;
|
this.buyDownFeeStrategy = builder.buyDownFeeStrategy;
|
||||||
this.buyDownFeeIncomeType = builder.buyDownFeeIncomeType;
|
this.buyDownFeeIncomeType = builder.buyDownFeeIncomeType;
|
||||||
|
this.interestMethod = builder.interestMethod;
|
||||||
|
this.allowPartialPeriodInterestCalcualtion = builder.allowPartialPeriodInterestCalculation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
|
private InterestMethod interestMethod;
|
||||||
private CurrencyData currency;
|
private CurrencyData currency;
|
||||||
private Integer loanTermFrequency;
|
private Integer loanTermFrequency;
|
||||||
private PeriodFrequencyType loanTermPeriodFrequencyType;
|
private PeriodFrequencyType loanTermPeriodFrequencyType;
|
||||||
@@ -327,6 +330,12 @@ public final class LoanApplicationTerms {
|
|||||||
private LoanBuyDownFeeCalculationType buyDownFeeCalculationType;
|
private LoanBuyDownFeeCalculationType buyDownFeeCalculationType;
|
||||||
private LoanBuyDownFeeStrategy buyDownFeeStrategy;
|
private LoanBuyDownFeeStrategy buyDownFeeStrategy;
|
||||||
private LoanBuyDownFeeIncomeType buyDownFeeIncomeType;
|
private LoanBuyDownFeeIncomeType buyDownFeeIncomeType;
|
||||||
|
private boolean allowPartialPeriodInterestCalculation;
|
||||||
|
|
||||||
|
public Builder interestMethod(InterestMethod interestMethod) {
|
||||||
|
this.interestMethod = interestMethod;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder currency(CurrencyData currency) {
|
public Builder currency(CurrencyData currency) {
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
@@ -491,6 +500,12 @@ public final class LoanApplicationTerms {
|
|||||||
this.daysInYearCustomStrategy = daysInYearCustomStrategy;
|
this.daysInYearCustomStrategy = daysInYearCustomStrategy;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder allowPartialPeriodInterestCalculation(boolean allowPartialPeriodInterestCalculation) {
|
||||||
|
this.allowPartialPeriodInterestCalculation = allowPartialPeriodInterestCalculation;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LoanApplicationTerms assembleFrom(LoanRepaymentScheduleModelData modelData, MathContext mc) {
|
public static LoanApplicationTerms assembleFrom(LoanRepaymentScheduleModelData modelData, MathContext mc) {
|
||||||
@@ -518,7 +533,8 @@ public final class LoanApplicationTerms {
|
|||||||
.isDownPaymentEnabled(modelData.downPaymentEnabled()).downPaymentPercentage(downPaymentPercentage)
|
.isDownPaymentEnabled(modelData.downPaymentEnabled()).downPaymentPercentage(downPaymentPercentage)
|
||||||
.submittedOnDate(modelData.scheduleGenerationStartDate()).seedDate(seedDate)
|
.submittedOnDate(modelData.scheduleGenerationStartDate()).seedDate(seedDate)
|
||||||
.interestRecognitionOnDisbursementDate(modelData.interestRecognitionOnDisbursementDate())
|
.interestRecognitionOnDisbursementDate(modelData.interestRecognitionOnDisbursementDate())
|
||||||
.daysInYearCustomStrategy(modelData.daysInYearCustomStrategy()).mc(mc).build();
|
.daysInYearCustomStrategy(modelData.daysInYearCustomStrategy()).interestMethod(modelData.interestMethod())
|
||||||
|
.allowPartialPeriodInterestCalculation(modelData.allowPartialPeriodInterestCalculation()).mc(mc).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LoanApplicationTerms assembleFrom(final CurrencyData currency, final Integer loanTermFrequency,
|
public static LoanApplicationTerms assembleFrom(final CurrencyData currency, final Integer loanTermFrequency,
|
||||||
@@ -526,7 +542,7 @@ public final class LoanApplicationTerms {
|
|||||||
final PeriodFrequencyType repaymentPeriodFrequencyType, Integer nthDay, DayOfWeekType weekDayType,
|
final PeriodFrequencyType repaymentPeriodFrequencyType, Integer nthDay, DayOfWeekType weekDayType,
|
||||||
final AmortizationMethod amortizationMethod, final InterestMethod interestMethod, final BigDecimal interestRatePerPeriod,
|
final AmortizationMethod amortizationMethod, final InterestMethod interestMethod, final BigDecimal interestRatePerPeriod,
|
||||||
final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualNominalInterestRate,
|
final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualNominalInterestRate,
|
||||||
final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final boolean allowPartialPeriodInterestCalcualtion,
|
final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final boolean allowPartialPeriodInterestCalculation,
|
||||||
final Money principalMoney, final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate,
|
final Money principalMoney, final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate,
|
||||||
final LocalDate calculatedRepaymentsStartingFromDate, final Integer graceOnPrincipalPayment,
|
final LocalDate calculatedRepaymentsStartingFromDate, final Integer graceOnPrincipalPayment,
|
||||||
final Integer recurringMoratoriumOnPrincipalPeriods, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged,
|
final Integer recurringMoratoriumOnPrincipalPeriods, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged,
|
||||||
@@ -561,7 +577,7 @@ public final class LoanApplicationTerms {
|
|||||||
return new LoanApplicationTerms(currency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery,
|
return new LoanApplicationTerms(currency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery,
|
||||||
repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod, interestRatePerPeriod,
|
repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod, interestRatePerPeriod,
|
||||||
interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
|
interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
|
||||||
allowPartialPeriodInterestCalcualtion, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate,
|
allowPartialPeriodInterestCalculation, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate,
|
||||||
calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods,
|
calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods,
|
||||||
graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount,
|
graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount,
|
||||||
disbursementDatas, maxOutstandingBalance, graceOnArrearsAgeing, daysInMonthType, daysInYearType,
|
disbursementDatas, maxOutstandingBalance, graceOnArrearsAgeing, daysInMonthType, daysInYearType,
|
||||||
@@ -608,7 +624,7 @@ public final class LoanApplicationTerms {
|
|||||||
final PeriodFrequencyType interestRatePeriodFrequencyType = loanProductRelatedDetail.getInterestPeriodFrequencyType();
|
final PeriodFrequencyType interestRatePeriodFrequencyType = loanProductRelatedDetail.getInterestPeriodFrequencyType();
|
||||||
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = loanProductRelatedDetail
|
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = loanProductRelatedDetail
|
||||||
.getInterestCalculationPeriodMethod();
|
.getInterestCalculationPeriodMethod();
|
||||||
final boolean allowPartialPeriodInterestCalcualtion = loanProductRelatedDetail.isAllowPartialPeriodInterestCalcualtion();
|
final boolean allowPartialPeriodInterestCalculation = loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation();
|
||||||
final Money principalMoney = loanProductRelatedDetail.getPrincipal();
|
final Money principalMoney = loanProductRelatedDetail.getPrincipal();
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -636,7 +652,7 @@ public final class LoanApplicationTerms {
|
|||||||
return new LoanApplicationTerms(currency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery,
|
return new LoanApplicationTerms(currency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery,
|
||||||
repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod, interestMethod,
|
repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod, interestMethod,
|
||||||
interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
|
interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
|
||||||
allowPartialPeriodInterestCalcualtion, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate,
|
allowPartialPeriodInterestCalculation, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate,
|
||||||
calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods,
|
calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods,
|
||||||
graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount,
|
graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount,
|
||||||
disbursementDatas, maxOutstandingBalance, loanProductRelatedDetail.getGraceOnArrearsAgeing(), daysInMonthType,
|
disbursementDatas, maxOutstandingBalance, loanProductRelatedDetail.getGraceOnArrearsAgeing(), daysInMonthType,
|
||||||
@@ -1674,7 +1690,7 @@ public final class LoanApplicationTerms {
|
|||||||
interestCalculationPeriodMethod, daysInYearType, daysInMonthType, amortizationMethod, repaymentPeriodFrequencyType,
|
interestCalculationPeriodMethod, daysInYearType, daysInMonthType, amortizationMethod, repaymentPeriodFrequencyType,
|
||||||
repaymentEvery, numberOfRepayments,
|
repaymentEvery, numberOfRepayments,
|
||||||
isInterestChargedFromDateSameAsDisbursalDateEnabled != null && isInterestChargedFromDateSameAsDisbursalDateEnabled,
|
isInterestChargedFromDateSameAsDisbursalDateEnabled != null && isInterestChargedFromDateSameAsDisbursalDateEnabled,
|
||||||
daysInYearCustomStrategy);
|
daysInYearCustomStrategy, allowPartialPeriodInterestCalcualtion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getLoanTermFrequency() {
|
public Integer getLoanTermFrequency() {
|
||||||
|
|||||||
+3
-1
@@ -27,11 +27,13 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
|||||||
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
|
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
|
||||||
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
|
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
|
||||||
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
|
||||||
|
|
||||||
public record LoanRepaymentScheduleModelData(@NotNull LocalDate scheduleGenerationStartDate, @NotNull CurrencyData currency,
|
public record LoanRepaymentScheduleModelData(@NotNull LocalDate scheduleGenerationStartDate, @NotNull CurrencyData currency,
|
||||||
@NotNull BigDecimal disbursementAmount, @NotNull LocalDate disbursementDate, @NotNull int numberOfRepayments,
|
@NotNull BigDecimal disbursementAmount, @NotNull LocalDate disbursementDate, @NotNull int numberOfRepayments,
|
||||||
@NotNull int repaymentFrequency, @NotBlank String repaymentFrequencyType, @NotNull BigDecimal annualNominalInterestRate,
|
@NotNull int repaymentFrequency, @NotBlank String repaymentFrequencyType, @NotNull BigDecimal annualNominalInterestRate,
|
||||||
@NotNull boolean downPaymentEnabled, @NotNull DaysInMonthType daysInMonth, @NotNull DaysInYearType daysInYear,
|
@NotNull boolean downPaymentEnabled, @NotNull DaysInMonthType daysInMonth, @NotNull DaysInYearType daysInYear,
|
||||||
BigDecimal downPaymentPercentage, Integer installmentAmountInMultiplesOf, Integer fixedLength,
|
BigDecimal downPaymentPercentage, Integer installmentAmountInMultiplesOf, Integer fixedLength,
|
||||||
@NotNull Boolean interestRecognitionOnDisbursementDate, @Nullable DaysInYearCustomStrategyType daysInYearCustomStrategy) {
|
@NotNull Boolean interestRecognitionOnDisbursementDate, @Nullable DaysInYearCustomStrategyType daysInYearCustomStrategy,
|
||||||
|
@NotNull InterestMethod interestMethod, @NotNull boolean allowPartialPeriodInterestCalculation) {
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -48,6 +48,7 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe
|
|||||||
private final Integer numberOfRepayments;
|
private final Integer numberOfRepayments;
|
||||||
private final boolean interestRecognitionOnDisbursementDate;
|
private final boolean interestRecognitionOnDisbursementDate;
|
||||||
private final DaysInYearCustomStrategyType daysInYearCustomStrategy;
|
private final DaysInYearCustomStrategyType daysInYearCustomStrategy;
|
||||||
|
private final boolean allowPartialPeriodInterestCalculation;
|
||||||
|
|
||||||
public LoanProductRelatedDetailMinimumData(CurrencyData currency, BigDecimal interestRatePerPeriod,
|
public LoanProductRelatedDetailMinimumData(CurrencyData currency, BigDecimal interestRatePerPeriod,
|
||||||
BigDecimal annualNominalInterestRate, Integer interestChargingGrace, Integer interestPaymentGrace, Integer principalGrace,
|
BigDecimal annualNominalInterestRate, Integer interestChargingGrace, Integer interestPaymentGrace, Integer principalGrace,
|
||||||
@@ -55,7 +56,7 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe
|
|||||||
InterestCalculationPeriodMethod interestCalculationPeriodMethod, DaysInYearType daysInYearType, DaysInMonthType daysInMonthType,
|
InterestCalculationPeriodMethod interestCalculationPeriodMethod, DaysInYearType daysInYearType, DaysInMonthType daysInMonthType,
|
||||||
AmortizationMethod amortizationMethod, PeriodFrequencyType repaymentPeriodFrequencyType, Integer repaymentEvery,
|
AmortizationMethod amortizationMethod, PeriodFrequencyType repaymentPeriodFrequencyType, Integer repaymentEvery,
|
||||||
Integer numberOfRepayments, boolean interestRecognitionOnDisbursementDate,
|
Integer numberOfRepayments, boolean interestRecognitionOnDisbursementDate,
|
||||||
DaysInYearCustomStrategyType daysInYearCustomStrategy) {
|
DaysInYearCustomStrategyType daysInYearCustomStrategy, boolean allowPartialPeriodInterestCalculation) {
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
this.interestRatePerPeriod = interestRatePerPeriod;
|
this.interestRatePerPeriod = interestRatePerPeriod;
|
||||||
this.annualNominalInterestRate = annualNominalInterestRate;
|
this.annualNominalInterestRate = annualNominalInterestRate;
|
||||||
@@ -73,6 +74,7 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe
|
|||||||
this.numberOfRepayments = numberOfRepayments;
|
this.numberOfRepayments = numberOfRepayments;
|
||||||
this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate;
|
this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate;
|
||||||
this.daysInYearCustomStrategy = daysInYearCustomStrategy;
|
this.daysInYearCustomStrategy = daysInYearCustomStrategy;
|
||||||
|
this.allowPartialPeriodInterestCalculation = allowPartialPeriodInterestCalculation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer defaultToNullIfZero(final Integer value) {
|
private Integer defaultToNullIfZero(final Integer value) {
|
||||||
@@ -83,6 +85,11 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe
|
|||||||
return defaultTo;
|
return defaultTo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAllowPartialPeriodInterestCalculation() {
|
||||||
|
return allowPartialPeriodInterestCalculation;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CurrencyData getCurrencyData() {
|
public CurrencyData getCurrencyData() {
|
||||||
return currency;
|
return currency;
|
||||||
|
|||||||
+4
@@ -53,4 +53,8 @@ public enum InterestCalculationPeriodMethod {
|
|||||||
return this.value.equals(InterestCalculationPeriodMethod.DAILY.getValue());
|
return this.value.equals(InterestCalculationPeriodMethod.DAILY.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSameAsRepaymentPeriod() {
|
||||||
|
return this.value.equals(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -52,4 +52,8 @@ public enum InterestMethod {
|
|||||||
public boolean isDecliningBalance() {
|
public boolean isDecliningBalance() {
|
||||||
return this.value.equals(InterestMethod.DECLINING_BALANCE.getValue());
|
return this.value.equals(InterestMethod.DECLINING_BALANCE.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isFlat() {
|
||||||
|
return this.value.equals(InterestMethod.FLAT.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -48,6 +48,8 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail {
|
|||||||
|
|
||||||
InterestCalculationPeriodMethod getInterestCalculationPeriodMethod();
|
InterestCalculationPeriodMethod getInterestCalculationPeriodMethod();
|
||||||
|
|
||||||
|
boolean isAllowPartialPeriodInterestCalculation();
|
||||||
|
|
||||||
Integer getRepayEvery();
|
Integer getRepayEvery();
|
||||||
|
|
||||||
PeriodFrequencyType getRepaymentPeriodFrequencyType();
|
PeriodFrequencyType getRepaymentPeriodFrequencyType();
|
||||||
|
|||||||
+3
-3
@@ -86,7 +86,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
|||||||
private InterestCalculationPeriodMethod interestCalculationPeriodMethod;
|
private InterestCalculationPeriodMethod interestCalculationPeriodMethod;
|
||||||
|
|
||||||
@Column(name = "allow_partial_period_interest_calcualtion", nullable = false)
|
@Column(name = "allow_partial_period_interest_calcualtion", nullable = false)
|
||||||
private boolean allowPartialPeriodInterestCalcualtion;
|
private boolean allowPartialPeriodInterestCalculation;
|
||||||
|
|
||||||
@Column(name = "repay_every", nullable = false)
|
@Column(name = "repay_every", nullable = false)
|
||||||
private Integer repayEvery;
|
private Integer repayEvery;
|
||||||
@@ -248,7 +248,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
|||||||
public LoanProductRelatedDetail(final MonetaryCurrency currency, final BigDecimal defaultPrincipal,
|
public LoanProductRelatedDetail(final MonetaryCurrency currency, final BigDecimal defaultPrincipal,
|
||||||
final BigDecimal defaultNominalInterestRatePerPeriod, final PeriodFrequencyType interestPeriodFrequencyType,
|
final BigDecimal defaultNominalInterestRatePerPeriod, final PeriodFrequencyType interestPeriodFrequencyType,
|
||||||
final BigDecimal defaultAnnualNominalInterestRate, final InterestMethod interestMethod,
|
final BigDecimal defaultAnnualNominalInterestRate, final InterestMethod interestMethod,
|
||||||
final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final boolean allowPartialPeriodInterestCalcualtion,
|
final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final boolean allowPartialPeriodInterestCalculation,
|
||||||
final Integer repayEvery, final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfRepayments,
|
final Integer repayEvery, final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfRepayments,
|
||||||
final Integer graceOnPrincipalPayment, final Integer recurringMoratoriumOnPrincipalPeriods,
|
final Integer graceOnPrincipalPayment, final Integer recurringMoratoriumOnPrincipalPeriods,
|
||||||
final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod,
|
final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod,
|
||||||
@@ -272,7 +272,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
|||||||
this.annualNominalInterestRate = defaultAnnualNominalInterestRate;
|
this.annualNominalInterestRate = defaultAnnualNominalInterestRate;
|
||||||
this.interestMethod = interestMethod;
|
this.interestMethod = interestMethod;
|
||||||
this.interestCalculationPeriodMethod = interestCalculationPeriodMethod;
|
this.interestCalculationPeriodMethod = interestCalculationPeriodMethod;
|
||||||
this.allowPartialPeriodInterestCalcualtion = allowPartialPeriodInterestCalcualtion;
|
this.allowPartialPeriodInterestCalculation = allowPartialPeriodInterestCalculation;
|
||||||
this.repayEvery = repayEvery;
|
this.repayEvery = repayEvery;
|
||||||
this.repaymentPeriodFrequencyType = repaymentFrequencyType;
|
this.repaymentPeriodFrequencyType = repaymentFrequencyType;
|
||||||
this.numberOfRepayments = defaultNumberOfRepayments;
|
this.numberOfRepayments = defaultNumberOfRepayments;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
|
|||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanRepaymentPeriod;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanRepaymentPeriod;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.EmbeddableProgressiveLoanScheduleGenerator;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.EmbeddableProgressiveLoanScheduleGenerator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanRepaymentScheduleModelData;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanRepaymentScheduleModelData;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
@@ -58,8 +59,10 @@ public class Main {
|
|||||||
final Integer fixedLength = null;
|
final Integer fixedLength = null;
|
||||||
final Boolean interestRecognitionOnDisbursementDate = false;
|
final Boolean interestRecognitionOnDisbursementDate = false;
|
||||||
final DaysInYearCustomStrategyType dasInYearCustomStrategy = null;
|
final DaysInYearCustomStrategyType dasInYearCustomStrategy = null;
|
||||||
|
final InterestMethod interestMethod = InterestMethod.DECLINING_BALANCE;
|
||||||
|
final boolean allowPartialPeriodInterestCalculation = true;
|
||||||
|
|
||||||
var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments, repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType, daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate, dasInYearCustomStrategy);
|
var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments, repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType, daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate, dasInYearCustomStrategy, interestMethod, allowPartialPeriodInterestCalculation);
|
||||||
|
|
||||||
final LoanSchedulePlan plan = calculator.generate(mc, config);
|
final LoanSchedulePlan plan = calculator.generate(mc, config);
|
||||||
printPlan(plan);
|
printPlan(plan);
|
||||||
|
|||||||
+4
-1
@@ -30,6 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
|
|||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanDisbursementPeriod;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanDisbursementPeriod;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanPeriod;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanPeriod;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanRepaymentPeriod;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanRepaymentPeriod;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -60,11 +61,13 @@ class EmbeddableProgressiveLoanScheduleGeneratorTest {
|
|||||||
final Integer fixedLength = null;
|
final Integer fixedLength = null;
|
||||||
final Boolean interestRecognitionOnDisbursementDate = false;
|
final Boolean interestRecognitionOnDisbursementDate = false;
|
||||||
final DaysInYearCustomStrategyType daysInYearCustomStrategy = null;
|
final DaysInYearCustomStrategyType daysInYearCustomStrategy = null;
|
||||||
|
final InterestMethod interestMethod = InterestMethod.DECLINING_BALANCE;
|
||||||
|
final boolean allowPartialPeriodInterestCalculation = true;
|
||||||
|
|
||||||
var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments,
|
var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments,
|
||||||
repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType,
|
repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType,
|
||||||
daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate,
|
daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate,
|
||||||
daysInYearCustomStrategy);
|
daysInYearCustomStrategy, interestMethod, allowPartialPeriodInterestCalculation);
|
||||||
|
|
||||||
final LoanSchedulePlan plan = calculator.generate(mc, config);
|
final LoanSchedulePlan plan = calculator.generate(mc, config);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ public class InterestScheduleModelServiceGsonContext {
|
|||||||
public RepaymentPeriod createRepaymentPeriodInstance(Type type) {
|
public RepaymentPeriod createRepaymentPeriodInstance(Type type) {
|
||||||
if (type == RepaymentPeriod.class) {
|
if (type == RepaymentPeriod.class) {
|
||||||
|
|
||||||
setPrev(RepaymentPeriod.empty(getPrev(), getMc()));
|
setPrev(RepaymentPeriod.empty(getPrev(), getMc(), getLoanProductRelatedDetail()));
|
||||||
return getPrev();
|
return getPrev();
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("Unsupported RepaymentPeriod type: " + type);
|
throw new IllegalArgumentException("Unsupported RepaymentPeriod type: " + type);
|
||||||
|
|||||||
+105
-5
@@ -35,6 +35,7 @@ import java.util.function.Predicate;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
|
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
|
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
|
||||||
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
|
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
|
||||||
@@ -91,7 +92,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
|
final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
|
||||||
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
|
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
|
||||||
List<RepaymentPeriod> repaymentPeriods = periods.stream().map(e -> {
|
List<RepaymentPeriod> repaymentPeriods = periods.stream().map(e -> {
|
||||||
RepaymentPeriod rp = RepaymentPeriod.create(prev.get(), from.apply(e), to.apply(e), zero, mc);
|
RepaymentPeriod rp = RepaymentPeriod.create(prev.get(), from.apply(e), to.apply(e), zero, mc, loanProductRelatedDetail);
|
||||||
prev.set(rp);
|
prev.set(rp);
|
||||||
return rp;
|
return rp;
|
||||||
}).toList();
|
}).toList();
|
||||||
@@ -114,10 +115,19 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
@Override
|
@Override
|
||||||
public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate,
|
public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate,
|
||||||
final Money disbursedAmount) {
|
final Money disbursedAmount) {
|
||||||
addDisbursement(scheduleModel, EmiChangeOperation.disburse(disbursementDueDate, disbursedAmount));
|
LocalDate effectiveDueDate = scheduleModel.loanProductRelatedDetail().getInterestCalculationPeriodMethod() != null
|
||||||
|
&& scheduleModel.loanProductRelatedDetail().getInterestCalculationPeriodMethod().isSameAsRepaymentPeriod()
|
||||||
|
&& !scheduleModel.loanProductRelatedDetail().isAllowPartialPeriodInterestCalculation()
|
||||||
|
? scheduleModel.repaymentPeriods().stream().filter(rp -> rp.getDueDate().isAfter(disbursementDueDate)).findFirst()
|
||||||
|
.map(RepaymentPeriod::getFromDate).orElse(disbursementDueDate)
|
||||||
|
: disbursementDueDate;
|
||||||
|
addDisbursement(scheduleModel, EmiChangeOperation.disburse(effectiveDueDate, disbursedAmount));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||||
|
scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate())).forEach(
|
||||||
|
rp -> rp.setTotalDisbursedAmount(MathUtil.nullToZero(rp.getTotalDisbursedAmount()).add(operation.getAmount().getAmount())));
|
||||||
|
|
||||||
scheduleModel
|
scheduleModel
|
||||||
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
|
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
|
||||||
scheduleModel.zero(), scheduleModel.zero())
|
scheduleModel.zero(), scheduleModel.zero())
|
||||||
@@ -444,11 +454,33 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
return scheduleModelCopy;
|
return scheduleModelCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void calculateEMIValueAndRateFactorsForFlatInterestMethod(final LocalDate calculateFromRepaymentPeriodDueDate,
|
||||||
|
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||||
|
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
|
||||||
|
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
|
||||||
|
if (relatedRepaymentPeriods.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
calculateEMIOnActualModelWithFlatInterestMethod(relatedRepaymentPeriods, scheduleModel);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest
|
* Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest
|
||||||
*/
|
*/
|
||||||
private void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaymentPeriodDueDate,
|
private void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaymentPeriodDueDate,
|
||||||
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||||
|
switch (scheduleModel.loanProductRelatedDetail().getInterestMethod()) {
|
||||||
|
case FLAT ->
|
||||||
|
calculateEMIValueAndRateFactorsForFlatInterestMethod(calculateFromRepaymentPeriodDueDate, scheduleModel, operation);
|
||||||
|
case DECLINING_BALANCE -> calculateEMIValueAndRateFactorsForDecliningBalanceInterestMethod(calculateFromRepaymentPeriodDueDate,
|
||||||
|
scheduleModel, operation);
|
||||||
|
default -> throw new UnsupportedOperationException(
|
||||||
|
"Unsupported interest method: " + scheduleModel.loanProductRelatedDetail().getInterestMethod());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void calculateEMIValueAndRateFactorsForDecliningBalanceInterestMethod(final LocalDate calculateFromRepaymentPeriodDueDate,
|
||||||
|
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||||
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
|
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
|
||||||
final boolean onlyOnActualModelShouldApply = scheduleModel.isEmpty()
|
final boolean onlyOnActualModelShouldApply = scheduleModel.isEmpty()
|
||||||
|| operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE || scheduleModel.isCopy();
|
|| operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE || scheduleModel.isCopy();
|
||||||
@@ -637,6 +669,20 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0
|
final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0
|
||||||
&& (!DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(daysInYearCustomStrategy)
|
&& (!DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(daysInYearCustomStrategy)
|
||||||
|| isPeriodContainsFeb29(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()));
|
|| isPeriodContainsFeb29(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()));
|
||||||
|
final BigDecimal repaymentEvery = BigDecimal.valueOf(loanProductRelatedDetail.getRepayEvery());
|
||||||
|
|
||||||
|
if (loanProductRelatedDetail.getInterestCalculationPeriodMethod() != null
|
||||||
|
&& loanProductRelatedDetail.getInterestCalculationPeriodMethod().isSameAsRepaymentPeriod()) {
|
||||||
|
|
||||||
|
if (loanProductRelatedDetail.getRepaymentPeriodFrequencyType().isMonthly()) {
|
||||||
|
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, BigDecimal.valueOf(12), actualDaysInPeriod,
|
||||||
|
calculatedDaysInPeriod, mc);
|
||||||
|
}
|
||||||
|
if (loanProductRelatedDetail.getRepaymentPeriodFrequencyType().isWeekly()) {
|
||||||
|
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, BigDecimal.valueOf(52), actualDaysInPeriod,
|
||||||
|
calculatedDaysInPeriod, mc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data
|
// TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data
|
||||||
// TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30)
|
// TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30)
|
||||||
@@ -747,13 +793,26 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate());
|
repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate());
|
||||||
final BigDecimal actualDaysInPeriod = BigDecimal
|
final BigDecimal actualDaysInPeriod = BigDecimal
|
||||||
.valueOf(DateUtils.getDifferenceInDays(interestPeriodFromDate, interestPeriodDueDate));
|
.valueOf(DateUtils.getDifferenceInDays(interestPeriodFromDate, interestPeriodDueDate));
|
||||||
final BigDecimal calculatedDaysInPeriod = BigDecimal
|
final BigDecimal calculatedDaysInRepaymentPeriod = BigDecimal
|
||||||
.valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()));
|
.valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()));
|
||||||
final int numberOfYearsDifferenceInPeriod = interestPeriodDueDate.getYear() - interestPeriodFromDate.getYear();
|
final int numberOfYearsDifferenceInPeriod = interestPeriodDueDate.getYear() - interestPeriodFromDate.getYear();
|
||||||
final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0
|
final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0
|
||||||
&& (!DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(daysInYearCustomStrategy)
|
&& (!DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(daysInYearCustomStrategy)
|
||||||
|| isPeriodContainsFeb29(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()));
|
|| isPeriodContainsFeb29(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()));
|
||||||
final BigDecimal daysInMonth = daysInMonthType.isDaysInMonth_30() ? BigDecimal.valueOf(30) : calculatedDaysInPeriod;
|
final BigDecimal daysInMonth = daysInMonthType.isDaysInMonth_30() ? BigDecimal.valueOf(30) : calculatedDaysInRepaymentPeriod;
|
||||||
|
|
||||||
|
if (loanProductRelatedDetail.getInterestCalculationPeriodMethod() != null
|
||||||
|
&& loanProductRelatedDetail.getInterestCalculationPeriodMethod().isSameAsRepaymentPeriod()) {
|
||||||
|
|
||||||
|
if (loanProductRelatedDetail.getRepaymentPeriodFrequencyType().isMonthly()) {
|
||||||
|
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, BigDecimal.valueOf(12), actualDaysInPeriod,
|
||||||
|
calculatedDaysInRepaymentPeriod, mc);
|
||||||
|
}
|
||||||
|
if (loanProductRelatedDetail.getRepaymentPeriodFrequencyType().isWeekly()) {
|
||||||
|
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, BigDecimal.valueOf(52), actualDaysInPeriod,
|
||||||
|
calculatedDaysInRepaymentPeriod, mc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data
|
// TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data
|
||||||
// TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30)
|
// TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30)
|
||||||
@@ -769,7 +828,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
case ACTUAL -> rateFactorByRepaymentPeriod(interestRate, actualDaysInPeriod, BigDecimal.ONE, daysInYear, BigDecimal.ONE,
|
case ACTUAL -> rateFactorByRepaymentPeriod(interestRate, actualDaysInPeriod, BigDecimal.ONE, daysInYear, BigDecimal.ONE,
|
||||||
BigDecimal.ONE, mc);
|
BigDecimal.ONE, mc);
|
||||||
case DAYS_30 -> calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, repaymentEvery,
|
case DAYS_30 -> calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, repaymentEvery,
|
||||||
daysInMonth, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
|
daysInMonth, daysInYear, actualDaysInPeriod, calculatedDaysInRepaymentPeriod, mc);
|
||||||
default -> throw new UnsupportedOperationException("Unsupported combination: Days in month: " + daysInMonthType);
|
default -> throw new UnsupportedOperationException("Unsupported combination: Days in month: " + daysInMonthType);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -845,10 +904,51 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void calculateEMIOnActualModelWithFlatInterestMethod(List<RepaymentPeriod> repaymentPeriods,
|
||||||
|
ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||||
|
|
||||||
|
final MathContext mc = scheduleModel.mc();
|
||||||
|
final CurrencyData currency = scheduleModel.loanProductRelatedDetail().getCurrencyData();
|
||||||
|
RepaymentPeriod first = repaymentPeriods.getFirst();
|
||||||
|
RepaymentPeriod last = repaymentPeriods.getLast();
|
||||||
|
Money sumOfInterest = Money.zero(currency);
|
||||||
|
for (RepaymentPeriod rp : repaymentPeriods) {
|
||||||
|
Money interest = rp.calculateCalculatedDueInterest();
|
||||||
|
sumOfInterest = sumOfInterest.add(interest);
|
||||||
|
rp.setEmi(interest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// already repaid principals should be subtracted from total disbursed amount to calculate correct EMI.
|
||||||
|
BigDecimal alreadyRepaidPrincipals = first.getPrevious()
|
||||||
|
.map(rp -> rp.calculateTotalDisbursedAmountTillGivenPeriod(null).subtract(rp.getOutstandingLoanBalance().getAmount()))
|
||||||
|
.orElse(BigDecimal.ZERO);
|
||||||
|
Money total = Money.of(currency, first.calculateTotalDisbursedAmountTillGivenPeriod(first.getLastInterestPeriod()))
|
||||||
|
.plus(sumOfInterest).minus(alreadyRepaidPrincipals);
|
||||||
|
|
||||||
|
Money periodEmi = total.dividedBy(repaymentPeriods.size(), mc);
|
||||||
|
Money remainder = total.minus(periodEmi.multipliedBy(repaymentPeriods.size(), mc));
|
||||||
|
|
||||||
|
repaymentPeriods.forEach(rp -> {
|
||||||
|
Money emi = rp.equals(last) ? periodEmi.add(remainder) : periodEmi;
|
||||||
|
rp.setEmi(emi);
|
||||||
|
rp.setOriginalEmi(emi);
|
||||||
|
rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void calculateEMIOnActualModel(List<RepaymentPeriod> repaymentPeriods, ProgressiveLoanInterestScheduleModel scheduleModel) {
|
private void calculateEMIOnActualModel(List<RepaymentPeriod> repaymentPeriods, ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||||
if (repaymentPeriods.isEmpty()) {
|
if (repaymentPeriods.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
switch (scheduleModel.loanProductRelatedDetail().getInterestMethod()) {
|
||||||
|
case FLAT -> calculateEMIOnActualModelWithFlatInterestMethod(repaymentPeriods, scheduleModel);
|
||||||
|
case DECLINING_BALANCE -> calculateEMIOnActualModelWithDecliningBalanceInterestMethod(repaymentPeriods, scheduleModel);
|
||||||
|
default -> throw new UnsupportedOperationException("Unsupported interest method");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void calculateEMIOnActualModelWithDecliningBalanceInterestMethod(List<RepaymentPeriod> repaymentPeriods,
|
||||||
|
ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||||
final MathContext mc = scheduleModel.mc();
|
final MathContext mc = scheduleModel.mc();
|
||||||
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(repaymentPeriods, mc));
|
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(repaymentPeriods, mc));
|
||||||
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods, mc));
|
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods, mc));
|
||||||
|
|||||||
+19
-7
@@ -33,6 +33,7 @@ import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude;
|
|||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ToString(exclude = { "repaymentPeriod" })
|
@ToString(exclude = { "repaymentPeriod" })
|
||||||
@@ -55,7 +56,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
|||||||
|
|
||||||
/** Stores credited principals. Related transactions: Chargeback or Credit Balance Refound */
|
/** Stores credited principals. Related transactions: Chargeback or Credit Balance Refound */
|
||||||
private Money creditedPrincipal;
|
private Money creditedPrincipal;
|
||||||
/** Stores credited principals. Related transaction: Chargeback */
|
/** Stores credited interest. Related transaction: Chargeback */
|
||||||
private Money creditedInterest;
|
private Money creditedInterest;
|
||||||
|
|
||||||
private Money disbursementAmount;
|
private Money disbursementAmount;
|
||||||
@@ -139,15 +140,26 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long lengthTillPeriodDueDate = getLengthTillPeriodDueDate();
|
long lengthTillPeriodDueDate = getLengthTillPeriodDueDate();
|
||||||
final BigDecimal interestDueTillRepaymentDueDate = lengthTillPeriodDueDate == 0 //
|
final BigDecimal interestDueTillRepaymentDueDate = getCalculatedDueInterest(
|
||||||
? BigDecimal.ZERO //
|
getRepaymentPeriod().getLoanProductRelatedDetail().getInterestMethod(), lengthTillPeriodDueDate); //
|
||||||
: getOutstandingLoanBalance().getAmount() //
|
|
||||||
.multiply(getRateFactorTillPeriodDueDate(), mc) //
|
|
||||||
.divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) //
|
|
||||||
.multiply(BigDecimal.valueOf(getLength()), mc); //
|
|
||||||
return MathUtil.negativeToZero(MathUtil.add(mc, creditedInterest.getAmount(), interestDueTillRepaymentDueDate));
|
return MathUtil.negativeToZero(MathUtil.add(mc, creditedInterest.getAmount(), interestDueTillRepaymentDueDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigDecimal getCalculatedDueInterest(InterestMethod method, long lengthTillPeriodDueDate) {
|
||||||
|
if (lengthTillPeriodDueDate == 0) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
BigDecimal baseAmount = switch (method) {
|
||||||
|
case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAmountTillGivenPeriod(this));
|
||||||
|
case DECLINING_BALANCE -> getOutstandingLoanBalance().getAmount();
|
||||||
|
default -> throw new UnsupportedOperationException("Method not implemented: " + method);
|
||||||
|
};
|
||||||
|
return baseAmount //
|
||||||
|
.multiply(getRateFactorTillPeriodDueDate(), mc) //
|
||||||
|
.divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) //
|
||||||
|
.multiply(BigDecimal.valueOf(getLength()), mc);
|
||||||
|
}
|
||||||
|
|
||||||
public long getLength() {
|
public long getLength() {
|
||||||
return DateUtils.getDifferenceInDays(fromDate, dueDate);
|
return DateUtils.getDifferenceInDays(fromDate, dueDate);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-11
@@ -199,18 +199,8 @@ public class ProgressiveLoanInterestScheduleModel {
|
|||||||
if (balanceChangeDate == null) {
|
if (balanceChangeDate == null) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
// TODO use isInPeriod
|
|
||||||
return repaymentPeriods.stream()//
|
return repaymentPeriods.stream()//
|
||||||
.filter(repaymentPeriod -> {
|
.filter(period -> isInPeriod(balanceChangeDate, period.getFromDate(), period.getDueDate(), period.isFirstRepaymentPeriod()))//
|
||||||
final boolean isFirstPeriod = repaymentPeriod.getPrevious().isEmpty();
|
|
||||||
if (isFirstPeriod) {
|
|
||||||
return !balanceChangeDate.isBefore(repaymentPeriod.getFromDate())
|
|
||||||
&& !balanceChangeDate.isAfter(repaymentPeriod.getDueDate());
|
|
||||||
} else {
|
|
||||||
return balanceChangeDate.isAfter(repaymentPeriod.getFromDate())
|
|
||||||
&& !balanceChangeDate.isAfter(repaymentPeriod.getDueDate());
|
|
||||||
}
|
|
||||||
})//
|
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
-12
@@ -34,6 +34,7 @@ import lombok.ToString;
|
|||||||
import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude;
|
import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
|
||||||
import org.apache.fineract.portfolio.util.Memo;
|
import org.apache.fineract.portfolio.util.Memo;
|
||||||
|
|
||||||
@ToString(exclude = { "previous" })
|
@ToString(exclude = { "previous" })
|
||||||
@@ -79,8 +80,17 @@ public final class RepaymentPeriod {
|
|||||||
@Setter
|
@Setter
|
||||||
private boolean isInterestMoved = false;
|
private boolean isInterestMoved = false;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
private BigDecimal totalDisbursedAmount;
|
||||||
|
|
||||||
|
@JsonExclude
|
||||||
|
@Getter
|
||||||
|
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
|
||||||
|
|
||||||
private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> interestPeriods,
|
private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> interestPeriods,
|
||||||
Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc) {
|
Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc,
|
||||||
|
LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail) {
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
this.fromDate = fromDate;
|
this.fromDate = fromDate;
|
||||||
this.dueDate = dueDate;
|
this.dueDate = dueDate;
|
||||||
@@ -91,16 +101,19 @@ public final class RepaymentPeriod {
|
|||||||
this.paidInterest = paidInterest;
|
this.paidInterest = paidInterest;
|
||||||
this.futureUnrecognizedInterest = futureUnrecognizedInterest;
|
this.futureUnrecognizedInterest = futureUnrecognizedInterest;
|
||||||
this.mc = mc;
|
this.mc = mc;
|
||||||
|
this.loanProductRelatedDetail = loanProductRelatedDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc) {
|
public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc,
|
||||||
return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, null, mc);
|
LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail) {
|
||||||
|
return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, null, mc, loanProductRelatedDetail);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc) {
|
public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc,
|
||||||
|
LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail) {
|
||||||
final Money zero = emi.zero();
|
final Money zero = emi.zero();
|
||||||
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero,
|
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero,
|
||||||
zero, mc);
|
zero, mc, loanProductRelatedDetail);
|
||||||
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
||||||
newRepaymentPeriod.interestPeriods.add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate));
|
newRepaymentPeriod.interestPeriods.add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate));
|
||||||
return newRepaymentPeriod;
|
return newRepaymentPeriod;
|
||||||
@@ -109,7 +122,8 @@ public final class RepaymentPeriod {
|
|||||||
public static RepaymentPeriod copy(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) {
|
public static RepaymentPeriod copy(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) {
|
||||||
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate,
|
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate,
|
||||||
new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, repaymentPeriod.paidPrincipal,
|
new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, repaymentPeriod.paidPrincipal,
|
||||||
repaymentPeriod.paidInterest, repaymentPeriod.futureUnrecognizedInterest, mc);
|
repaymentPeriod.paidInterest, repaymentPeriod.futureUnrecognizedInterest, mc,
|
||||||
|
repaymentPeriod.getLoanProductRelatedDetail());
|
||||||
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
||||||
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
|
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
|
||||||
newRepaymentPeriod.interestPeriods.add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc));
|
newRepaymentPeriod.interestPeriods.add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc));
|
||||||
@@ -120,7 +134,8 @@ public final class RepaymentPeriod {
|
|||||||
public static RepaymentPeriod copyWithoutPaidAmounts(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) {
|
public static RepaymentPeriod copyWithoutPaidAmounts(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) {
|
||||||
final Money zero = repaymentPeriod.emi.zero();
|
final Money zero = repaymentPeriod.emi.zero();
|
||||||
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate,
|
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate,
|
||||||
new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, zero, zero, zero, mc);
|
new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, zero, zero, zero, mc,
|
||||||
|
repaymentPeriod.getLoanProductRelatedDetail());
|
||||||
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
||||||
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
|
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
|
||||||
var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod);
|
var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod);
|
||||||
@@ -160,13 +175,13 @@ public final class RepaymentPeriod {
|
|||||||
@NotNull
|
@NotNull
|
||||||
public Money getCalculatedDueInterest() {
|
public Money getCalculatedDueInterest() {
|
||||||
if (calculatedDueInterestCalculation == null) {
|
if (calculatedDueInterestCalculation == null) {
|
||||||
calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest,
|
calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, () -> new Object[] { this.previous,
|
||||||
() -> new Object[] { this.previous, this.interestPeriods, this.futureUnrecognizedInterest, this.isInterestMoved });
|
this.interestPeriods, this.futureUnrecognizedInterest, this.isInterestMoved, this.totalDisbursedAmount });
|
||||||
}
|
}
|
||||||
return calculatedDueInterestCalculation.get();
|
return calculatedDueInterestCalculation.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Money calculateCalculatedDueInterest() {
|
public Money calculateCalculatedDueInterest() {
|
||||||
Money calculatedDueInterest = getZero(mc);
|
Money calculatedDueInterest = getZero(mc);
|
||||||
if (!isInterestMoved) {
|
if (!isInterestMoved) {
|
||||||
calculatedDueInterest = Money.of(emi.getCurrencyData(),
|
calculatedDueInterest = Money.of(emi.getCurrencyData(),
|
||||||
@@ -192,7 +207,8 @@ public final class RepaymentPeriod {
|
|||||||
() -> MathUtil.max(getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest()
|
() -> MathUtil.max(getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest()
|
||||||
: MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest(), false),
|
: MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest(), false),
|
||||||
getPaidInterest(), false),
|
getPaidInterest(), false),
|
||||||
() -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest });
|
() -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest, totalDisbursedAmount,
|
||||||
|
emi });
|
||||||
}
|
}
|
||||||
return dueInterestCalculation.get();
|
return dueInterestCalculation.get();
|
||||||
}
|
}
|
||||||
@@ -307,7 +323,7 @@ public final class RepaymentPeriod {
|
|||||||
.minus(getDuePrincipal(), mc)//
|
.minus(getDuePrincipal(), mc)//
|
||||||
.plus(getPaidPrincipal(), mc);//
|
.plus(getPaidPrincipal(), mc);//
|
||||||
return MathUtil.negativeToZero(calculatedOutStandingLoanBalance, mc);
|
return MathUtil.negativeToZero(calculatedOutStandingLoanBalance, mc);
|
||||||
}, () -> new Object[] { paidPrincipal, paidInterest, interestPeriods });
|
}, () -> new Object[] { paidPrincipal, paidInterest, interestPeriods, totalDisbursedAmount });
|
||||||
}
|
}
|
||||||
return outstandingBalanceCalculation.get();
|
return outstandingBalanceCalculation.get();
|
||||||
}
|
}
|
||||||
@@ -369,4 +385,22 @@ public final class RepaymentPeriod {
|
|||||||
this.paidInterest = paidInterest.zero();
|
this.paidInterest = paidInterest.zero();
|
||||||
this.paidPrincipal = paidPrincipal.zero();
|
this.paidPrincipal = paidPrincipal.zero();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tillPeriod
|
||||||
|
* can be null. if null it calculates total disbursement including last interest period.
|
||||||
|
* @return disbursed amount til interest period.
|
||||||
|
*/
|
||||||
|
public BigDecimal calculateTotalDisbursedAmountTillGivenPeriod(InterestPeriod tillPeriod) {
|
||||||
|
BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount());
|
||||||
|
for (InterestPeriod interestPeriod : this.interestPeriods) {
|
||||||
|
if (interestPeriod.equals(tillPeriod)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!interestPeriod.getDueDate().equals(getFromDate()) && interestPeriod.getDisbursementAmount() != null) {
|
||||||
|
res = res.add(interestPeriod.getDisbursementAmount().getAmount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -36,6 +36,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
|
|||||||
import org.apache.fineract.portfolio.loanaccount.service.InterestScheduleModelRepositoryWrapper;
|
import org.apache.fineract.portfolio.loanaccount.service.InterestScheduleModelRepositoryWrapper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
|
||||||
import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator;
|
import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
@@ -63,7 +64,8 @@ class LoanScheduleGeneratorTest {
|
|||||||
void testGenerateLoanSchedule() {
|
void testGenerateLoanSchedule() {
|
||||||
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY,
|
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY,
|
||||||
DISBURSEMENT_AMOUNT, DISBURSEMENT_DATE, NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
|
DISBURSEMENT_AMOUNT, DISBURSEMENT_DATE, NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
|
||||||
NOMINAL_INTEREST_RATE, false, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null, false, null);
|
NOMINAL_INTEREST_RATE, false, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null, false, null,
|
||||||
|
InterestMethod.DECLINING_BALANCE, true);
|
||||||
|
|
||||||
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
|
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
|
||||||
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator,
|
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator,
|
||||||
@@ -101,7 +103,7 @@ class LoanScheduleGeneratorTest {
|
|||||||
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY,
|
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY,
|
||||||
DISBURSEMENT_AMOUNT_100, LocalDate.of(2024, 1, 1), NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
|
DISBURSEMENT_AMOUNT_100, LocalDate.of(2024, 1, 1), NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
|
||||||
NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, DOWN_PAYMENT_PORTION, null, null, false,
|
NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, DOWN_PAYMENT_PORTION, null, null, false,
|
||||||
null);
|
null, InterestMethod.DECLINING_BALANCE, true);
|
||||||
|
|
||||||
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
|
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
|
||||||
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator,
|
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator,
|
||||||
|
|||||||
+406
@@ -24,6 +24,8 @@ import java.math.RoundingMode;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||||
@@ -42,6 +44,8 @@ import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
|
|||||||
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
||||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||||
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
|
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
@@ -56,6 +60,7 @@ import org.mockito.Mockito;
|
|||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ProgressiveEMICalculatorTest {
|
class ProgressiveEMICalculatorTest {
|
||||||
|
|
||||||
@@ -99,6 +104,9 @@ class ProgressiveEMICalculatorTest {
|
|||||||
public void setupTestDefaults() {
|
public void setupTestDefaults() {
|
||||||
Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false);
|
Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false);
|
||||||
Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()).thenReturn(null);
|
Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()).thenReturn(null);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()).thenReturn(InterestCalculationPeriodMethod.DAILY);
|
||||||
|
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigDecimal getRateFactorsByMonth(final DaysInYearType daysInYearType, final DaysInMonthType daysInMonthType,
|
private BigDecimal getRateFactorsByMonth(final DaysInYearType daysInYearType, final DaysInMonthType daysInMonthType,
|
||||||
@@ -3226,6 +3234,404 @@ class ProgressiveEMICalculatorTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
public class InterestTypeFlat {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setupTestDefaults() {
|
||||||
|
Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()).thenReturn(null);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.FLAT);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||||
|
// InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD
|
||||||
|
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
|
||||||
|
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
|
||||||
|
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod())
|
||||||
|
.thenReturn(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD);
|
||||||
|
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_sameAsRepayment_days_repay_every_6_periods_5() {
|
||||||
|
|
||||||
|
final BigDecimal interestRate = BigDecimal.valueOf(12.2);
|
||||||
|
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.DAYS);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(5);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(6);
|
||||||
|
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||||
|
final Integer installmentAmountInMultiplesOf = null;
|
||||||
|
|
||||||
|
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||||
|
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 202.00, 2.00, 200.00, 800.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 202.00, 2.00, 200.00, 600.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 202.00, 2.00, 200.00, 400.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 3, 202.00, 2.00, 200.00, 200.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 4, 202.00, 2.00, 200.00, 0.00, false); //
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_sameAsRepayment_week_repay_every_1_periods_10() {
|
||||||
|
|
||||||
|
final BigDecimal interestRate = BigDecimal.valueOf(5.2);
|
||||||
|
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
|
||||||
|
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = expectedRepaymentWeeks(disbursementDate, 10, 1);
|
||||||
|
final Integer installmentAmountInMultiplesOf = null;
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_364.getValue());
|
||||||
|
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod())
|
||||||
|
.thenReturn(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||||
|
|
||||||
|
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||||
|
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 101.00, 1.00, 100.00, 900.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 101.00, 1.00, 100.00, 800.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 101.00, 1.00, 100.00, 700.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 3, 101.00, 1.00, 100.00, 600.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 4, 101.00, 1.00, 100.00, 500.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 5, 101.00, 1.00, 100.00, 400.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 6, 101.00, 1.00, 100.00, 300.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 7, 101.00, 1.00, 100.00, 200.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 8, 101.00, 1.00, 100.00, 100.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 9, 101.00, 1.00, 100.00, 0.00, false); //
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_sameAsRepayment_month_repay_every_2_periods_8() {
|
||||||
|
|
||||||
|
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
|
||||||
|
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(8);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
|
||||||
|
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||||
|
final Integer installmentAmountInMultiplesOf = null;
|
||||||
|
|
||||||
|
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||||
|
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 145.00, 20.00, 125.00, 875.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 145.00, 20.00, 125.00, 750.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 145.00, 20.00, 125.00, 625.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 3, 145.00, 20.00, 125.00, 500.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 4, 145.00, 20.00, 125.00, 375.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 5, 145.00, 20.00, 125.00, 250.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 6, 145.00, 20.00, 125.00, 125.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 7, 145.00, 20.00, 125.00, 0.00, false); //
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_sameAsRepayment_month_repay_every_1_periods_3_second_disbursement_on_repayment_due_date_allow_partial() {
|
||||||
|
|
||||||
|
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
|
||||||
|
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
|
||||||
|
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||||
|
|
||||||
|
final Integer installmentAmountInMultiplesOf = null;
|
||||||
|
|
||||||
|
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||||
|
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 343.33, 10.00, 333.33, 333.34, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 343.34, 10.00, 333.34, 0.00, false); //
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusMonths(1), toMoney(250.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 916.67, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 470.84, 12.50, 458.34, 458.33, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 470.83, 12.50, 458.33, 0.00, false); //
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_sameAsRepayment_month_repay_every_1_periods_3_second_disbursement_on_repayment_due_date_disallow_partial() {
|
||||||
|
|
||||||
|
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
|
||||||
|
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(false);
|
||||||
|
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||||
|
|
||||||
|
final Integer installmentAmountInMultiplesOf = null;
|
||||||
|
|
||||||
|
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||||
|
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 343.33, 10.00, 333.33, 333.34, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 343.34, 10.00, 333.34, 0.00, false); //
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusMonths(1), toMoney(250.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 916.67, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 470.84, 12.50, 458.34, 458.33, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 470.83, 12.50, 458.33, 0.00, false); //
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_sameAsRepayment_month_repay_every_1_periods_3() {
|
||||||
|
|
||||||
|
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
|
||||||
|
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||||
|
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||||
|
|
||||||
|
final Integer installmentAmountInMultiplesOf = null;
|
||||||
|
|
||||||
|
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||||
|
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 343.33, 10.00, 333.33, 333.34, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 343.34, 10.00, 333.34, 0.00, false); //
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusDays(4), toMoney(250.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 429.06, 12.18, 416.88, 833.12, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 429.06, 12.50, 416.56, 416.56, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 429.06, 12.50, 416.56, 0.00, false); //
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusDays(4), toMoney(250.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 514.78, 14.35, 500.43, 999.57, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 514.78, 15.00, 499.78, 499.79, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 514.79, 15.00, 499.79, 0.00, false); //
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_sameAsRepayment_month_repay_every_1_periods_20() {
|
||||||
|
|
||||||
|
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
|
||||||
|
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(20);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||||
|
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||||
|
final Integer installmentAmountInMultiplesOf = null;
|
||||||
|
|
||||||
|
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||||
|
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 60.00, 10.00, 50.00, 950.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 60.00, 10.00, 50.00, 900.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 60.00, 10.00, 50.00, 850.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 3, 60.00, 10.00, 50.00, 800.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 4, 60.00, 10.00, 50.00, 750.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 5, 60.00, 10.00, 50.00, 700.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 6, 60.00, 10.00, 50.00, 650.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 7, 60.00, 10.00, 50.00, 600.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 8, 60.00, 10.00, 50.00, 550.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 9, 60.00, 10.00, 50.00, 500.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 10, 60.00, 10.00, 50.00, 450.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 11, 60.00, 10.00, 50.00, 400.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 12, 60.00, 10.00, 50.00, 350.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 13, 60.00, 10.00, 50.00, 300.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 14, 60.00, 10.00, 50.00, 250.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 15, 60.00, 10.00, 50.00, 200.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 16, 60.00, 10.00, 50.00, 150.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 17, 60.00, 10.00, 50.00, 100.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 18, 60.00, 10.00, 50.00, 50.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 19, 60.00, 10.00, 50.00, 0.00, false); //
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_sameAsRepayment_month_repay_every_1_periods_24() {
|
||||||
|
|
||||||
|
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
|
||||||
|
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(24);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||||
|
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||||
|
final Integer installmentAmountInMultiplesOf = null;
|
||||||
|
|
||||||
|
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||||
|
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(7500.0));
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(2500.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 516.67, 100.00, 416.67, 9583.33, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 516.67, 100.00, 416.67, 9166.66, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 516.67, 100.00, 416.67, 8749.99, false); //
|
||||||
|
checkPeriod(interestSchedule, 3, 516.67, 100.00, 416.67, 8333.32, false); //
|
||||||
|
checkPeriod(interestSchedule, 4, 516.67, 100.00, 416.67, 7916.65, false); //
|
||||||
|
checkPeriod(interestSchedule, 5, 516.67, 100.00, 416.67, 7499.98, false); //
|
||||||
|
checkPeriod(interestSchedule, 6, 516.67, 100.00, 416.67, 7083.31, false); //
|
||||||
|
checkPeriod(interestSchedule, 7, 516.67, 100.00, 416.67, 6666.64, false); //
|
||||||
|
checkPeriod(interestSchedule, 8, 516.67, 100.00, 416.67, 6249.97, false); //
|
||||||
|
checkPeriod(interestSchedule, 9, 516.67, 100.00, 416.67, 5833.30, false); //
|
||||||
|
checkPeriod(interestSchedule, 10, 516.67, 100.00, 416.67, 5416.63, false); //
|
||||||
|
checkPeriod(interestSchedule, 11, 516.67, 100.00, 416.67, 4999.96, false); //
|
||||||
|
checkPeriod(interestSchedule, 12, 516.67, 100.00, 416.67, 4583.29, false); //
|
||||||
|
checkPeriod(interestSchedule, 13, 516.67, 100.00, 416.67, 4166.62, false); //
|
||||||
|
checkPeriod(interestSchedule, 14, 516.67, 100.00, 416.67, 3749.95, false); //
|
||||||
|
checkPeriod(interestSchedule, 15, 516.67, 100.00, 416.67, 3333.28, false); //
|
||||||
|
checkPeriod(interestSchedule, 16, 516.67, 100.00, 416.67, 2916.61, false); //
|
||||||
|
checkPeriod(interestSchedule, 17, 516.67, 100.00, 416.67, 2499.94, false); //
|
||||||
|
checkPeriod(interestSchedule, 18, 516.67, 100.00, 416.67, 2083.27, false); //
|
||||||
|
checkPeriod(interestSchedule, 19, 516.67, 100.00, 416.67, 1666.60, false); //
|
||||||
|
checkPeriod(interestSchedule, 20, 516.67, 100.00, 416.67, 1249.93, false); //
|
||||||
|
checkPeriod(interestSchedule, 21, 516.67, 100.00, 416.67, 833.26, false); //
|
||||||
|
checkPeriod(interestSchedule, 22, 516.67, 100.00, 416.67, 416.59, false); //
|
||||||
|
checkPeriod(interestSchedule, 23, 516.59, 100.00, 416.59, 0.00, false); //
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusMonths(3).plusDays(4), toMoney(1000.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 516.67, 100.00, 416.67, 9583.33, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 516.67, 100.00, 416.67, 9166.66, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 516.67, 100.00, 416.67, 8749.99, false); //
|
||||||
|
checkPeriod(interestSchedule, 3, 574.22, 108.67, 465.55, 9284.44, false); //
|
||||||
|
checkPeriod(interestSchedule, 4, 574.22, 110.00, 464.22, 8820.22, false); //
|
||||||
|
checkPeriod(interestSchedule, 5, 574.22, 110.00, 464.22, 8356.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 6, 574.22, 110.00, 464.22, 7891.78, false); //
|
||||||
|
checkPeriod(interestSchedule, 7, 574.22, 110.00, 464.22, 7427.56, false); //
|
||||||
|
checkPeriod(interestSchedule, 8, 574.22, 110.00, 464.22, 6963.34, false); //
|
||||||
|
checkPeriod(interestSchedule, 9, 574.22, 110.00, 464.22, 6499.12, false); //
|
||||||
|
checkPeriod(interestSchedule, 10, 574.22, 110.00, 464.22, 6034.90, false); //
|
||||||
|
checkPeriod(interestSchedule, 11, 574.22, 110.00, 464.22, 5570.68, false); //
|
||||||
|
checkPeriod(interestSchedule, 12, 574.22, 110.00, 464.22, 5106.46, false); //
|
||||||
|
checkPeriod(interestSchedule, 13, 574.22, 110.00, 464.22, 4642.24, false); //
|
||||||
|
checkPeriod(interestSchedule, 14, 574.22, 110.00, 464.22, 4178.02, false); //
|
||||||
|
checkPeriod(interestSchedule, 15, 574.22, 110.00, 464.22, 3713.80, false); //
|
||||||
|
checkPeriod(interestSchedule, 16, 574.22, 110.00, 464.22, 3249.58, false); //
|
||||||
|
checkPeriod(interestSchedule, 17, 574.22, 110.00, 464.22, 2785.36, false); //
|
||||||
|
checkPeriod(interestSchedule, 18, 574.22, 110.00, 464.22, 2321.14, false); //
|
||||||
|
checkPeriod(interestSchedule, 19, 574.22, 110.00, 464.22, 1856.92, false); //
|
||||||
|
checkPeriod(interestSchedule, 20, 574.22, 110.00, 464.22, 1392.70, false); //
|
||||||
|
checkPeriod(interestSchedule, 21, 574.22, 110.00, 464.22, 928.48, false); //
|
||||||
|
checkPeriod(interestSchedule, 22, 574.22, 110.00, 464.22, 464.26, false); //
|
||||||
|
checkPeriod(interestSchedule, 23, 574.26, 110.00, 464.26, 0.00, false); //
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_sameAsRepayment_month_repay_every_1_periods_3__not_calculate_exact_days() {
|
||||||
|
|
||||||
|
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
|
||||||
|
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
|
||||||
|
|
||||||
|
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
|
||||||
|
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||||
|
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(false);
|
||||||
|
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||||
|
|
||||||
|
final Integer installmentAmountInMultiplesOf = null;
|
||||||
|
|
||||||
|
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||||
|
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
|
||||||
|
|
||||||
|
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 343.33, 10.00, 333.33, 333.34, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 343.34, 10.00, 333.34, 0.00, false); //
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusDays(4), toMoney(250.0));
|
||||||
|
checkPeriod(interestSchedule, 0, 429.17, 12.50, 416.67, 833.33, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 429.17, 12.50, 416.67, 416.66, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 429.16, 12.50, 416.66, 0.00, false); //
|
||||||
|
|
||||||
|
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusDays(17), toMoney(250.0));
|
||||||
|
checkPeriod(interestSchedule, 0, 515.00, 15.00, 500.00, 1000.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 1, 515.00, 15.00, 500.00, 500.00, false); //
|
||||||
|
checkPeriod(interestSchedule, 2, 515.00, 15.00, 500.00, 0.00, false); //
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// utilities
|
||||||
|
private List<LoanScheduleModelRepaymentPeriod> generateExpectedRepaymentPeriods(LocalDate disbursementDate) {
|
||||||
|
return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {
|
||||||
|
case MONTHS -> expectedRepaymentsMonthly(disbursementDate, loanProductRelatedDetail.getNumberOfRepayments(),
|
||||||
|
loanProductRelatedDetail.getRepayEvery());
|
||||||
|
case WEEKS -> expectedRepaymentWeeks(disbursementDate, loanProductRelatedDetail.getNumberOfRepayments(),
|
||||||
|
loanProductRelatedDetail.getRepayEvery());
|
||||||
|
case DAYS -> expectedRepaymentDays(disbursementDate, loanProductRelatedDetail.getNumberOfRepayments(),
|
||||||
|
loanProductRelatedDetail.getRepayEvery());
|
||||||
|
default -> throw new UnsupportedOperationException();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LoanScheduleModelRepaymentPeriod> expectedRepaymentDays(final LocalDate disbursementDate, final int periods, final int length) {
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>(periods);
|
||||||
|
IntStream.range(0, periods).forEach(i -> expectedRepaymentPeriods
|
||||||
|
.add(repayment(i + 1, disbursementDate.plusDays((long) i * length), disbursementDate.plusDays((long) (i + 1) * length))));
|
||||||
|
return expectedRepaymentPeriods;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LoanScheduleModelRepaymentPeriod> expectedRepaymentsMonthly(final LocalDate disbursementDate, final int periods,
|
||||||
|
final int length) {
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>(periods);
|
||||||
|
IntStream.range(0, periods).forEach(i -> expectedRepaymentPeriods.add(
|
||||||
|
repayment(i + 1, disbursementDate.plusMonths((long) i * length), disbursementDate.plusMonths((long) (i + 1) * length))));
|
||||||
|
return expectedRepaymentPeriods;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LoanScheduleModelRepaymentPeriod> expectedRepaymentWeeks(final LocalDate disbursementDate, final int periods, final int length) {
|
||||||
|
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>(periods);
|
||||||
|
IntStream.range(0, periods).forEach(i -> expectedRepaymentPeriods
|
||||||
|
.add(repayment(i + 1, disbursementDate.plusWeeks((long) i * length), disbursementDate.plusWeeks((long) (i + 1) * length))));
|
||||||
|
return expectedRepaymentPeriods;
|
||||||
|
}
|
||||||
|
|
||||||
private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {
|
private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {
|
||||||
final Money zeroAmount = Money.zero(currency);
|
final Money zeroAmount = Money.zero(currency);
|
||||||
return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,
|
return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,
|
||||||
|
|||||||
+4
-4
@@ -232,7 +232,7 @@ public class LoanScheduleAssembler {
|
|||||||
Boolean allowPartialPeriodInterestCalcualtion = this.fromApiJsonHelper
|
Boolean allowPartialPeriodInterestCalcualtion = this.fromApiJsonHelper
|
||||||
.extractBooleanNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, element);
|
.extractBooleanNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, element);
|
||||||
if (allowPartialPeriodInterestCalcualtion == null) {
|
if (allowPartialPeriodInterestCalcualtion == null) {
|
||||||
allowPartialPeriodInterestCalcualtion = loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalcualtion();
|
allowPartialPeriodInterestCalcualtion = loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation();
|
||||||
}
|
}
|
||||||
|
|
||||||
final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element);
|
final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element);
|
||||||
@@ -1359,15 +1359,15 @@ public class LoanScheduleAssembler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME,
|
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME,
|
||||||
loanProductRelatedDetail.isAllowPartialPeriodInterestCalcualtion())) {
|
loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation())) {
|
||||||
final boolean newValue = command
|
final boolean newValue = command
|
||||||
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME);
|
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME);
|
||||||
changes.put(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, newValue);
|
changes.put(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, newValue);
|
||||||
loanProductRelatedDetail.setAllowPartialPeriodInterestCalcualtion(newValue);
|
loanProductRelatedDetail.setAllowPartialPeriodInterestCalculation(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loanProductRelatedDetail.getInterestCalculationPeriodMethod().isDaily()) {
|
if (loanProductRelatedDetail.getInterestCalculationPeriodMethod().isDaily()) {
|
||||||
loanProductRelatedDetail.setAllowPartialPeriodInterestCalcualtion(false);
|
loanProductRelatedDetail.setAllowPartialPeriodInterestCalculation(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String graceOnPrincipalPaymentParamName = "graceOnPrincipalPayment";
|
final String graceOnPrincipalPaymentParamName = "graceOnPrincipalPayment";
|
||||||
|
|||||||
+36
-9
@@ -1667,14 +1667,20 @@ public final class LoanApplicationValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateLoanMultiDisbursementDate(final JsonElement element, LocalDate expectedDisbursementDate, BigDecimal principal) {
|
public void validateLoanMultiDisbursementDate(final JsonElement element, LocalDate expectedDisbursementDate, BigDecimal principal,
|
||||||
|
Loan loan) {
|
||||||
Validator.validateOrThrow("loan", baseDataValidator -> {
|
Validator.validateOrThrow("loan", baseDataValidator -> {
|
||||||
validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal);
|
validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal, loan);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateLoanMultiDisbursementDate(final JsonElement element, final DataValidatorBuilder baseDataValidator,
|
public void validateLoanMultiDisbursementDate(final JsonElement element, final DataValidatorBuilder baseDataValidator,
|
||||||
LocalDate expectedDisbursement, BigDecimal totalPrincipal) {
|
LocalDate expectedDisbursement, BigDecimal totalPrincipal) {
|
||||||
|
validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursement, totalPrincipal, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateLoanMultiDisbursementDate(final JsonElement element, final DataValidatorBuilder baseDataValidator,
|
||||||
|
LocalDate expectedDisbursement, BigDecimal totalPrincipal, Loan loan) {
|
||||||
this.validateDisbursementsAreDatewiseOrdered(element, baseDataValidator);
|
this.validateDisbursementsAreDatewiseOrdered(element, baseDataValidator);
|
||||||
|
|
||||||
final JsonObject topLevelJsonElement = element.getAsJsonObject();
|
final JsonObject topLevelJsonElement = element.getAsJsonObject();
|
||||||
@@ -1732,11 +1738,31 @@ public final class LoanApplicationValidator {
|
|||||||
baseDataValidator.reset().parameter(LoanApiConstants.disbursementPrincipalParameterName)
|
baseDataValidator.reset().parameter(LoanApiConstants.disbursementPrincipalParameterName)
|
||||||
.failWithCode(LoanApiConstants.APPROVED_AMOUNT_IS_LESS_THAN_SUM_OF_TRANCHES);
|
.failWithCode(LoanApiConstants.APPROVED_AMOUNT_IS_LESS_THAN_SUM_OF_TRANCHES);
|
||||||
}
|
}
|
||||||
final Integer interestType = this.fromApiJsonHelper
|
|
||||||
.extractIntegerSansLocaleNamed(LoanApiConstants.interestTypeParameterName, element);
|
|
||||||
baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType).ignoreIfNull()
|
|
||||||
.integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue());
|
|
||||||
|
|
||||||
|
if (loan == null) {
|
||||||
|
final String transactionProcessingStrategyCode = this.fromApiJsonHelper
|
||||||
|
.extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element);
|
||||||
|
if (transactionProcessingStrategyCode != null) {
|
||||||
|
final Integer interestType = this.fromApiJsonHelper.extractIntegerNamed(LoanApiConstants.interestTypeParameterName,
|
||||||
|
element, Locale.getDefault());
|
||||||
|
String processorCode = loanRepaymentScheduleTransactionProcessorFactory
|
||||||
|
.determineProcessor(transactionProcessingStrategyCode).getCode();
|
||||||
|
boolean isProgressive = "advanced-payment-allocation-strategy".equals(processorCode);
|
||||||
|
if (isProgressive) {
|
||||||
|
baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType)
|
||||||
|
.ignoreIfNull().inMinMaxRange(0, 1);
|
||||||
|
} else {
|
||||||
|
baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType)
|
||||||
|
.ignoreIfNull().integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (loan.isCumulativeSchedule()) {
|
||||||
|
baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName)
|
||||||
|
.value(loan.getLoanProductRelatedDetail().getInterestMethod()).ignoreIfNull()
|
||||||
|
.value(InterestMethod.DECLINING_BALANCE);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1756,7 +1782,7 @@ public final class LoanApplicationValidator {
|
|||||||
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod
|
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod
|
||||||
.fromInt(interestCalculationPeriodType);
|
.fromInt(interestCalculationPeriodType);
|
||||||
boolean considerPartialPeriodUpdates = interestCalculationPeriodMethod.isDaily() ? interestCalculationPeriodMethod.isDaily()
|
boolean considerPartialPeriodUpdates = interestCalculationPeriodMethod.isDaily() ? interestCalculationPeriodMethod.isDaily()
|
||||||
: loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalcualtion();
|
: loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation();
|
||||||
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME,
|
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME,
|
||||||
element)) {
|
element)) {
|
||||||
final Boolean considerPartialInterestEnabled = this.fromApiJsonHelper
|
final Boolean considerPartialInterestEnabled = this.fromApiJsonHelper
|
||||||
@@ -1780,7 +1806,8 @@ public final class LoanApplicationValidator {
|
|||||||
.failWithCode("not.supported.for.selected.interest.calcualtion.type");
|
.failWithCode("not.supported.for.selected.interest.calcualtion.type");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loanProduct.isMultiDisburseLoan()) {
|
if (loanProduct.isMultiDisburseLoan()
|
||||||
|
&& !"advanced-payment-allocation-strategy".equals(loanProduct.getTransactionProcessingStrategyCode())) {
|
||||||
baseDataValidator.reset().parameter(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME)
|
baseDataValidator.reset().parameter(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME)
|
||||||
.failWithCode("not.supported.for.selected.interest.calcualtion.type");
|
.failWithCode("not.supported.for.selected.interest.calcualtion.type");
|
||||||
}
|
}
|
||||||
@@ -2045,7 +2072,7 @@ public final class LoanApplicationValidator {
|
|||||||
|
|
||||||
LoanProduct loanProduct = loan.loanProduct();
|
LoanProduct loanProduct = loan.loanProduct();
|
||||||
if (loanProduct.isMultiDisburseLoan()) {
|
if (loanProduct.isMultiDisburseLoan()) {
|
||||||
validateLoanMultiDisbursementDate(element, expectedDisbursementDate, principal);
|
validateLoanMultiDisbursementDate(element, expectedDisbursementDate, principal, loan);
|
||||||
|
|
||||||
final JsonArray disbursementDataArray = this.fromApiJsonHelper
|
final JsonArray disbursementDataArray = this.fromApiJsonHelper
|
||||||
.extractJsonArrayNamed(LoanApiConstants.disbursementDataParameterName, element);
|
.extractJsonArrayNamed(LoanApiConstants.disbursementDataParameterName, element);
|
||||||
|
|||||||
+3
-3
@@ -201,15 +201,15 @@ public class LoanProductRelatedDetailUpdateUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME,
|
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME,
|
||||||
loanRepaymentScheduleDetail.isAllowPartialPeriodInterestCalcualtion())) {
|
loanRepaymentScheduleDetail.isAllowPartialPeriodInterestCalculation())) {
|
||||||
final boolean newValue = command
|
final boolean newValue = command
|
||||||
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME);
|
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME);
|
||||||
actualChanges.put(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, newValue);
|
actualChanges.put(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, newValue);
|
||||||
loanRepaymentScheduleDetail.setAllowPartialPeriodInterestCalcualtion(newValue);
|
loanRepaymentScheduleDetail.setAllowPartialPeriodInterestCalculation(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loanRepaymentScheduleDetail.getInterestCalculationPeriodMethod().isDaily()) {
|
if (loanRepaymentScheduleDetail.getInterestCalculationPeriodMethod().isDaily()) {
|
||||||
loanRepaymentScheduleDetail.setAllowPartialPeriodInterestCalcualtion(false);
|
loanRepaymentScheduleDetail.setAllowPartialPeriodInterestCalculation(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String graceOnPrincipalPaymentParamName = "graceOnPrincipalPayment";
|
final String graceOnPrincipalPaymentParamName = "graceOnPrincipalPayment";
|
||||||
|
|||||||
+3
-3
@@ -2232,7 +2232,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void validateMultiDisbursementData(final JsonCommand command, LocalDate expectedDisbursementDate,
|
private void validateMultiDisbursementData(final JsonCommand command, LocalDate expectedDisbursementDate,
|
||||||
boolean isDisallowExpectedDisbursements) {
|
boolean isDisallowExpectedDisbursements, Loan loan) {
|
||||||
final String json = command.json();
|
final String json = command.json();
|
||||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
|
||||||
@@ -2254,7 +2254,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
|
|
||||||
final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("approvedLoanAmount", element);
|
final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("approvedLoanAmount", element);
|
||||||
|
|
||||||
loanApplicationValidator.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal);
|
loanApplicationValidator.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal, loan);
|
||||||
if (!dataValidationErrors.isEmpty()) {
|
if (!dataValidationErrors.isEmpty()) {
|
||||||
throw new PlatformApiDataValidationException(dataValidationErrors);
|
throw new PlatformApiDataValidationException(dataValidationErrors);
|
||||||
}
|
}
|
||||||
@@ -2296,7 +2296,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
final String errorMessage = "cannot.modify.tranches.if.loan.is.pendingapproval.closed.overpaid.writtenoff";
|
final String errorMessage = "cannot.modify.tranches.if.loan.is.pendingapproval.closed.overpaid.writtenoff";
|
||||||
throw new LoanMultiDisbursementException(errorMessage);
|
throw new LoanMultiDisbursementException(errorMessage);
|
||||||
}
|
}
|
||||||
validateMultiDisbursementData(command, expectedDisbursementDate, loan.loanProduct().isDisallowExpectedDisbursements());
|
validateMultiDisbursementData(command, expectedDisbursementDate, loan.loanProduct().isDisallowExpectedDisbursements(), loan);
|
||||||
|
|
||||||
this.validateForAddAndDeleteTranche(loan);
|
this.validateForAddAndDeleteTranche(loan);
|
||||||
|
|
||||||
|
|||||||
+28
-7
@@ -786,7 +786,7 @@ public final class LoanProductDataValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateMultiDisburseLoanData(baseDataValidator, element);
|
validateMultiDisburseLoanData(baseDataValidator, element, null);
|
||||||
|
|
||||||
validateLoanConfigurableAttributes(baseDataValidator, element);
|
validateLoanConfigurableAttributes(baseDataValidator, element);
|
||||||
|
|
||||||
@@ -1014,7 +1014,8 @@ public final class LoanProductDataValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateMultiDisburseLoanData(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
private void validateMultiDisburseLoanData(final DataValidatorBuilder baseDataValidator, final JsonElement element,
|
||||||
|
final LoanProduct loanProduct) {
|
||||||
Boolean multiDisburseLoan = false;
|
Boolean multiDisburseLoan = false;
|
||||||
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME, element)) {
|
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME, element)) {
|
||||||
multiDisburseLoan = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME,
|
multiDisburseLoan = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME,
|
||||||
@@ -1045,14 +1046,34 @@ public final class LoanProductDataValidator {
|
|||||||
.integerGreaterThanZero();
|
.integerGreaterThanZero();
|
||||||
|
|
||||||
final Integer interestType = this.fromApiJsonHelper.extractIntegerNamed(INTEREST_TYPE, element, Locale.getDefault());
|
final Integer interestType = this.fromApiJsonHelper.extractIntegerNamed(INTEREST_TYPE, element, Locale.getDefault());
|
||||||
baseDataValidator.reset().parameter(INTEREST_TYPE).value(interestType).ignoreIfNull()
|
|
||||||
.integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue());
|
boolean isProgressive = isProgressive(element, loanProduct);
|
||||||
|
if (isProgressive) {
|
||||||
|
baseDataValidator.reset().parameter(INTEREST_TYPE).value(interestType).ignoreIfNull().inMinMaxRange(0, 1);
|
||||||
|
} else {
|
||||||
|
baseDataValidator.reset().parameter(INTEREST_TYPE).value(interestType).ignoreIfNull()
|
||||||
|
.integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final String overAppliedCalculationType = this.fromApiJsonHelper.extractStringNamed(OVER_APPLIED_CALCULATION_TYPE, element);
|
final String overAppliedCalculationType = this.fromApiJsonHelper.extractStringNamed(OVER_APPLIED_CALCULATION_TYPE, element);
|
||||||
baseDataValidator.reset().parameter(OVER_APPLIED_CALCULATION_TYPE).value(overAppliedCalculationType).notExceedingLengthOf(10);
|
baseDataValidator.reset().parameter(OVER_APPLIED_CALCULATION_TYPE).value(overAppliedCalculationType).notExceedingLengthOf(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isProgressive(JsonElement element, LoanProduct loanProduct) {
|
||||||
|
String processorCode = null;
|
||||||
|
if (loanProduct != null) {
|
||||||
|
processorCode = loanProduct.getTransactionProcessingStrategyCode();
|
||||||
|
}
|
||||||
|
final String transactionProcessingStrategyCode = this.fromApiJsonHelper.extractStringNamed(TRANSACTION_PROCESSING_STRATEGY_CODE,
|
||||||
|
element);
|
||||||
|
if (transactionProcessingStrategyCode != null) {
|
||||||
|
processorCode = loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode)
|
||||||
|
.getCode();
|
||||||
|
}
|
||||||
|
return "advanced-payment-allocation-strategy".equals(processorCode);
|
||||||
|
}
|
||||||
|
|
||||||
private void validateInterestRecalculationParams(final JsonElement element, final DataValidatorBuilder baseDataValidator,
|
private void validateInterestRecalculationParams(final JsonElement element, final DataValidatorBuilder baseDataValidator,
|
||||||
final LoanProduct loanProduct) {
|
final LoanProduct loanProduct) {
|
||||||
|
|
||||||
@@ -1868,7 +1889,7 @@ public final class LoanProductDataValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateMultiDisburseLoanData(baseDataValidator, element);
|
validateMultiDisburseLoanData(baseDataValidator, element, loanProduct);
|
||||||
|
|
||||||
// validateLoanConfigurableAttributes(baseDataValidator,element);
|
// validateLoanConfigurableAttributes(baseDataValidator,element);
|
||||||
|
|
||||||
@@ -2549,7 +2570,7 @@ public final class LoanProductDataValidator {
|
|||||||
}
|
}
|
||||||
} else if (loanProduct != null) {
|
} else if (loanProduct != null) {
|
||||||
if (!interestCalculationPeriodMethod.isDaily()) {
|
if (!interestCalculationPeriodMethod.isDaily()) {
|
||||||
considerPartialPeriodUpdates = loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalcualtion();
|
considerPartialPeriodUpdates = loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2574,7 +2595,7 @@ public final class LoanProductDataValidator {
|
|||||||
} else if (loanProduct != null) {
|
} else if (loanProduct != null) {
|
||||||
multiDisburseLoan = loanProduct.isMultiDisburseLoan();
|
multiDisburseLoan = loanProduct.isMultiDisburseLoan();
|
||||||
}
|
}
|
||||||
if (multiDisburseLoan != null && multiDisburseLoan) {
|
if (multiDisburseLoan != null && multiDisburseLoan && !isProgressive(element, loanProduct)) {
|
||||||
baseDataValidator.reset().parameter(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME)
|
baseDataValidator.reset().parameter(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME)
|
||||||
.failWithCode("not.supported.for.selected.interest.calculation.type");
|
.failWithCode("not.supported.for.selected.interest.calculation.type");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user