FINERACT-1968: Progressive Loan schedule handling - interest handling (FLAT interest, Same as repayment period, Equal installments)

This commit is contained in:
Soma Sörös
2025-07-09 14:39:18 +02:00
committed by Adam Saghy
parent 3f359bb368
commit 40f8b59ab4
21 changed files with 710 additions and 77 deletions
@@ -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() {
@@ -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) {
} }
@@ -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;
@@ -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());
}
} }
@@ -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());
}
} }
@@ -48,6 +48,8 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail {
InterestCalculationPeriodMethod getInterestCalculationPeriodMethod(); InterestCalculationPeriodMethod getInterestCalculationPeriodMethod();
boolean isAllowPartialPeriodInterestCalculation();
Integer getRepayEvery(); Integer getRepayEvery();
PeriodFrequencyType getRepaymentPeriodFrequencyType(); PeriodFrequencyType getRepaymentPeriodFrequencyType();
@@ -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);
@@ -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);
@@ -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);
@@ -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));
@@ -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);
} }
@@ -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();
} }
@@ -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;
}
} }
@@ -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,
@@ -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,
@@ -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";
@@ -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);
@@ -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";
@@ -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);
@@ -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");
} }