This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 40f8b59ab44d1dc15c844871f4749a68789dd544 Author: Soma Sörös <[email protected]> AuthorDate: Wed Jun 25 15:30:29 2025 +0200 FINERACT-1968: Progressive Loan schedule handling - interest handling (FLAT interest, Same as repayment period, Equal installments) --- .../loanschedule/domain/LoanApplicationTerms.java | 28 +- .../domain/LoanRepaymentScheduleModelData.java | 4 +- .../data/LoanProductRelatedDetailMinimumData.java | 9 +- .../domain/InterestCalculationPeriodMethod.java | 4 + .../loanproduct/domain/InterestMethod.java | 4 + ...oductMinimumRepaymentScheduleRelatedDetail.java | 2 + .../domain/LoanProductRelatedDetail.java | 6 +- .../misc/Main.java | 5 +- ...ddableProgressiveLoanScheduleGeneratorTest.java | 5 +- .../InterestScheduleModelServiceGsonContext.java | 2 +- .../loanproduct/calc/ProgressiveEMICalculator.java | 110 +++++- .../loanproduct/calc/data/InterestPeriod.java | 26 +- .../data/ProgressiveLoanInterestScheduleModel.java | 12 +- .../loanproduct/calc/data/RepaymentPeriod.java | 58 ++- .../domain/LoanScheduleGeneratorTest.java | 6 +- .../calc/ProgressiveEMICalculatorTest.java | 406 +++++++++++++++++++++ .../service/LoanScheduleAssembler.java | 8 +- .../serialization/LoanApplicationValidator.java | 45 ++- .../LoanProductRelatedDetailUpdateUtil.java | 6 +- .../LoanWritePlatformServiceJpaRepositoryImpl.java | 6 +- .../serialization/LoanProductDataValidator.java | 35 +- 21 files changed, 710 insertions(+), 77 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index 96e13f7452..0ec0d57e8c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -290,10 +290,13 @@ public final class LoanApplicationTerms { this.buyDownFeeCalculationType = builder.buyDownFeeCalculationType; this.buyDownFeeStrategy = builder.buyDownFeeStrategy; this.buyDownFeeIncomeType = builder.buyDownFeeIncomeType; + this.interestMethod = builder.interestMethod; + this.allowPartialPeriodInterestCalcualtion = builder.allowPartialPeriodInterestCalculation; } public static class Builder { + private InterestMethod interestMethod; private CurrencyData currency; private Integer loanTermFrequency; private PeriodFrequencyType loanTermPeriodFrequencyType; @@ -327,6 +330,12 @@ public final class LoanApplicationTerms { private LoanBuyDownFeeCalculationType buyDownFeeCalculationType; private LoanBuyDownFeeStrategy buyDownFeeStrategy; private LoanBuyDownFeeIncomeType buyDownFeeIncomeType; + private boolean allowPartialPeriodInterestCalculation; + + public Builder interestMethod(InterestMethod interestMethod) { + this.interestMethod = interestMethod; + return this; + } public Builder currency(CurrencyData currency) { this.currency = currency; @@ -491,6 +500,12 @@ public final class LoanApplicationTerms { this.daysInYearCustomStrategy = daysInYearCustomStrategy; return this; } + + public Builder allowPartialPeriodInterestCalculation(boolean allowPartialPeriodInterestCalculation) { + this.allowPartialPeriodInterestCalculation = allowPartialPeriodInterestCalculation; + return this; + } + } public static LoanApplicationTerms assembleFrom(LoanRepaymentScheduleModelData modelData, MathContext mc) { @@ -518,7 +533,8 @@ public final class LoanApplicationTerms { .isDownPaymentEnabled(modelData.downPaymentEnabled()).downPaymentPercentage(downPaymentPercentage) .submittedOnDate(modelData.scheduleGenerationStartDate()).seedDate(seedDate) .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, @@ -526,7 +542,7 @@ public final class LoanApplicationTerms { final PeriodFrequencyType repaymentPeriodFrequencyType, Integer nthDay, DayOfWeekType weekDayType, final AmortizationMethod amortizationMethod, final InterestMethod interestMethod, final BigDecimal interestRatePerPeriod, 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 LocalDate calculatedRepaymentsStartingFromDate, final Integer graceOnPrincipalPayment, 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, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, - allowPartialPeriodInterestCalcualtion, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, + allowPartialPeriodInterestCalculation, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount, disbursementDatas, maxOutstandingBalance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, @@ -608,7 +624,7 @@ public final class LoanApplicationTerms { final PeriodFrequencyType interestRatePeriodFrequencyType = loanProductRelatedDetail.getInterestPeriodFrequencyType(); final InterestCalculationPeriodMethod interestCalculationPeriodMethod = loanProductRelatedDetail .getInterestCalculationPeriodMethod(); - final boolean allowPartialPeriodInterestCalcualtion = loanProductRelatedDetail.isAllowPartialPeriodInterestCalcualtion(); + final boolean allowPartialPeriodInterestCalculation = loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation(); final Money principalMoney = loanProductRelatedDetail.getPrincipal(); // @@ -636,7 +652,7 @@ public final class LoanApplicationTerms { return new LoanApplicationTerms(currency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, - allowPartialPeriodInterestCalcualtion, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, + allowPartialPeriodInterestCalculation, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount, disbursementDatas, maxOutstandingBalance, loanProductRelatedDetail.getGraceOnArrearsAgeing(), daysInMonthType, @@ -1674,7 +1690,7 @@ public final class LoanApplicationTerms { interestCalculationPeriodMethod, daysInYearType, daysInMonthType, amortizationMethod, repaymentPeriodFrequencyType, repaymentEvery, numberOfRepayments, isInterestChargedFromDateSameAsDisbursalDateEnabled != null && isInterestChargedFromDateSameAsDisbursalDateEnabled, - daysInYearCustomStrategy); + daysInYearCustomStrategy, allowPartialPeriodInterestCalcualtion); } public Integer getLoanTermFrequency() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java index 286f4d8a04..fe6b1e055a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java @@ -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.DaysInYearCustomStrategyType; 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, @NotNull BigDecimal disbursementAmount, @NotNull LocalDate disbursementDate, @NotNull int numberOfRepayments, @NotNull int repaymentFrequency, @NotBlank String repaymentFrequencyType, @NotNull BigDecimal annualNominalInterestRate, @NotNull boolean downPaymentEnabled, @NotNull DaysInMonthType daysInMonth, @NotNull DaysInYearType daysInYear, BigDecimal downPaymentPercentage, Integer installmentAmountInMultiplesOf, Integer fixedLength, - @NotNull Boolean interestRecognitionOnDisbursementDate, @Nullable DaysInYearCustomStrategyType daysInYearCustomStrategy) { + @NotNull Boolean interestRecognitionOnDisbursementDate, @Nullable DaysInYearCustomStrategyType daysInYearCustomStrategy, + @NotNull InterestMethod interestMethod, @NotNull boolean allowPartialPeriodInterestCalculation) { } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java index ffdfc47d75..812b158588 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java @@ -48,6 +48,7 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe private final Integer numberOfRepayments; private final boolean interestRecognitionOnDisbursementDate; private final DaysInYearCustomStrategyType daysInYearCustomStrategy; + private final boolean allowPartialPeriodInterestCalculation; public LoanProductRelatedDetailMinimumData(CurrencyData currency, BigDecimal interestRatePerPeriod, BigDecimal annualNominalInterestRate, Integer interestChargingGrace, Integer interestPaymentGrace, Integer principalGrace, @@ -55,7 +56,7 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe InterestCalculationPeriodMethod interestCalculationPeriodMethod, DaysInYearType daysInYearType, DaysInMonthType daysInMonthType, AmortizationMethod amortizationMethod, PeriodFrequencyType repaymentPeriodFrequencyType, Integer repaymentEvery, Integer numberOfRepayments, boolean interestRecognitionOnDisbursementDate, - DaysInYearCustomStrategyType daysInYearCustomStrategy) { + DaysInYearCustomStrategyType daysInYearCustomStrategy, boolean allowPartialPeriodInterestCalculation) { this.currency = currency; this.interestRatePerPeriod = interestRatePerPeriod; this.annualNominalInterestRate = annualNominalInterestRate; @@ -73,6 +74,7 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe this.numberOfRepayments = numberOfRepayments; this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate; this.daysInYearCustomStrategy = daysInYearCustomStrategy; + this.allowPartialPeriodInterestCalculation = allowPartialPeriodInterestCalculation; } private Integer defaultToNullIfZero(final Integer value) { @@ -83,6 +85,11 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe return defaultTo; } + @Override + public boolean isAllowPartialPeriodInterestCalculation() { + return allowPartialPeriodInterestCalculation; + } + @Override public CurrencyData getCurrencyData() { return currency; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/InterestCalculationPeriodMethod.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/InterestCalculationPeriodMethod.java index 1c0237fdb7..1e5ed160c5 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/InterestCalculationPeriodMethod.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/InterestCalculationPeriodMethod.java @@ -53,4 +53,8 @@ public enum InterestCalculationPeriodMethod { return this.value.equals(InterestCalculationPeriodMethod.DAILY.getValue()); } + public boolean isSameAsRepaymentPeriod() { + return this.value.equals(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD.getValue()); + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/InterestMethod.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/InterestMethod.java index a511378427..407f7fcc38 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/InterestMethod.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/InterestMethod.java @@ -52,4 +52,8 @@ public enum InterestMethod { public boolean isDecliningBalance() { return this.value.equals(InterestMethod.DECLINING_BALANCE.getValue()); } + + public boolean isFlat() { + return this.value.equals(InterestMethod.FLAT.getValue()); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java index 08623b60b1..10d33bef0b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java @@ -48,6 +48,8 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail { InterestCalculationPeriodMethod getInterestCalculationPeriodMethod(); + boolean isAllowPartialPeriodInterestCalculation(); + Integer getRepayEvery(); PeriodFrequencyType getRepaymentPeriodFrequencyType(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index c200f657bf..dcbf1a9046 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -86,7 +86,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche private InterestCalculationPeriodMethod interestCalculationPeriodMethod; @Column(name = "allow_partial_period_interest_calcualtion", nullable = false) - private boolean allowPartialPeriodInterestCalcualtion; + private boolean allowPartialPeriodInterestCalculation; @Column(name = "repay_every", nullable = false) private Integer repayEvery; @@ -248,7 +248,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche public LoanProductRelatedDetail(final MonetaryCurrency currency, final BigDecimal defaultPrincipal, final BigDecimal defaultNominalInterestRatePerPeriod, final PeriodFrequencyType interestPeriodFrequencyType, 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 graceOnPrincipalPayment, final Integer recurringMoratoriumOnPrincipalPeriods, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, @@ -272,7 +272,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.annualNominalInterestRate = defaultAnnualNominalInterestRate; this.interestMethod = interestMethod; this.interestCalculationPeriodMethod = interestCalculationPeriodMethod; - this.allowPartialPeriodInterestCalcualtion = allowPartialPeriodInterestCalcualtion; + this.allowPartialPeriodInterestCalculation = allowPartialPeriodInterestCalculation; this.repayEvery = repayEvery; this.repaymentPeriodFrequencyType = repaymentFrequencyType; this.numberOfRepayments = defaultNumberOfRepayments; diff --git a/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java b/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java index 5e08658f6e..1b7d006eb6 100644 --- a/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java +++ b/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java @@ -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.domain.EmbeddableProgressiveLoanScheduleGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanRepaymentScheduleModelData; +import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; import java.math.BigDecimal; import java.math.MathContext; @@ -58,8 +59,10 @@ public class Main { final Integer fixedLength = null; final Boolean interestRecognitionOnDisbursementDate = false; 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); printPlan(plan); diff --git a/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java b/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java index 2899cdf114..0eea0ff98f 100644 --- a/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java +++ b/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java @@ -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.LoanSchedulePlanPeriod; 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.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -60,11 +61,13 @@ class EmbeddableProgressiveLoanScheduleGeneratorTest { final Integer fixedLength = null; final Boolean interestRecognitionOnDisbursementDate = false; final DaysInYearCustomStrategyType daysInYearCustomStrategy = 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, - daysInYearCustomStrategy); + daysInYearCustomStrategy, interestMethod, allowPartialPeriodInterestCalculation); final LoanSchedulePlan plan = calculator.generate(mc, config); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelServiceGsonContext.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelServiceGsonContext.java index 39de64dfe1..767c817bca 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelServiceGsonContext.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelServiceGsonContext.java @@ -42,7 +42,7 @@ public class InterestScheduleModelServiceGsonContext { public RepaymentPeriod createRepaymentPeriodInstance(Type type) { if (type == RepaymentPeriod.class) { - setPrev(RepaymentPeriod.empty(getPrev(), getMc())); + setPrev(RepaymentPeriod.empty(getPrev(), getMc(), getLoanProductRelatedDetail())); return getPrev(); } throw new IllegalArgumentException("Unsupported RepaymentPeriod type: " + type); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index f4f84359ce..66aed18cc7 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -35,6 +35,7 @@ import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; 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.portfolio.common.domain.DaysInMonthType; 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 AtomicReference<RepaymentPeriod> prev = new AtomicReference<>(); 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); return rp; }).toList(); @@ -114,10 +115,19 @@ public final class ProgressiveEMICalculator implements EMICalculator { @Override public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate, 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) { + scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate())).forEach( + rp -> rp.setTotalDisbursedAmount(MathUtil.nullToZero(rp.getTotalDisbursedAmount()).add(operation.getAmount().getAmount()))); + scheduleModel .changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(), scheduleModel.zero(), scheduleModel.zero()) @@ -444,11 +454,33 @@ public final class ProgressiveEMICalculator implements EMICalculator { 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 */ private void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaymentPeriodDueDate, 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 boolean onlyOnActualModelShouldApply = scheduleModel.isEmpty() || 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 && (!DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(daysInYearCustomStrategy) || 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 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()); final BigDecimal actualDaysInPeriod = BigDecimal .valueOf(DateUtils.getDifferenceInDays(interestPeriodFromDate, interestPeriodDueDate)); - final BigDecimal calculatedDaysInPeriod = BigDecimal + final BigDecimal calculatedDaysInRepaymentPeriod = BigDecimal .valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate())); final int numberOfYearsDifferenceInPeriod = interestPeriodDueDate.getYear() - interestPeriodFromDate.getYear(); final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0 && (!DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(daysInYearCustomStrategy) || 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 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, BigDecimal.ONE, mc); 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); }; } @@ -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) { if (repaymentPeriods.isEmpty()) { 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 BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(repaymentPeriods, mc)); final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods, mc)); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java index d38280f281..4b1be251c8 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java @@ -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.MathUtil; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; @Getter @ToString(exclude = { "repaymentPeriod" }) @@ -55,7 +56,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> { /** Stores credited principals. Related transactions: Chargeback or Credit Balance Refound */ private Money creditedPrincipal; - /** Stores credited principals. Related transaction: Chargeback */ + /** Stores credited interest. Related transaction: Chargeback */ private Money creditedInterest; private Money disbursementAmount; @@ -139,15 +140,26 @@ public class InterestPeriod implements Comparable<InterestPeriod> { } long lengthTillPeriodDueDate = getLengthTillPeriodDueDate(); - final BigDecimal interestDueTillRepaymentDueDate = lengthTillPeriodDueDate == 0 // - ? BigDecimal.ZERO // - : getOutstandingLoanBalance().getAmount() // - .multiply(getRateFactorTillPeriodDueDate(), mc) // - .divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) // - .multiply(BigDecimal.valueOf(getLength()), mc); // + final BigDecimal interestDueTillRepaymentDueDate = getCalculatedDueInterest( + getRepaymentPeriod().getLoanProductRelatedDetail().getInterestMethod(), lengthTillPeriodDueDate); // 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() { return DateUtils.getDifferenceInDays(fromDate, dueDate); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java index c7c4410adc..f17b782dbe 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java @@ -199,18 +199,8 @@ public class ProgressiveLoanInterestScheduleModel { if (balanceChangeDate == null) { return Optional.empty(); } - // TODO use isInPeriod return repaymentPeriods.stream()// - .filter(repaymentPeriod -> { - 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()); - } - })// + .filter(period -> isInPeriod(balanceChangeDate, period.getFromDate(), period.getDueDate(), period.isFirstRepaymentPeriod()))// .findFirst(); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java index 33b94d852c..0778e436e8 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java @@ -34,6 +34,7 @@ import lombok.ToString; import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; import org.apache.fineract.portfolio.util.Memo; @ToString(exclude = { "previous" }) @@ -79,8 +80,17 @@ public final class RepaymentPeriod { @Setter 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, - 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.fromDate = fromDate; this.dueDate = dueDate; @@ -91,16 +101,19 @@ public final class RepaymentPeriod { this.paidInterest = paidInterest; this.futureUnrecognizedInterest = futureUnrecognizedInterest; this.mc = mc; + this.loanProductRelatedDetail = loanProductRelatedDetail; } - public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc) { - return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, null, mc); + public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext 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 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 newRepaymentPeriod.interestPeriods.add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate)); return newRepaymentPeriod; @@ -109,7 +122,8 @@ public final class RepaymentPeriod { public static RepaymentPeriod copy(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) { final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate, 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 for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) { 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) { final Money zero = repaymentPeriod.emi.zero(); 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 for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) { var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod); @@ -160,13 +175,13 @@ public final class RepaymentPeriod { @NotNull public Money getCalculatedDueInterest() { if (calculatedDueInterestCalculation == null) { - calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, - () -> new Object[] { this.previous, this.interestPeriods, this.futureUnrecognizedInterest, this.isInterestMoved }); + calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, () -> new Object[] { this.previous, + this.interestPeriods, this.futureUnrecognizedInterest, this.isInterestMoved, this.totalDisbursedAmount }); } return calculatedDueInterestCalculation.get(); } - private Money calculateCalculatedDueInterest() { + public Money calculateCalculatedDueInterest() { Money calculatedDueInterest = getZero(mc); if (!isInterestMoved) { calculatedDueInterest = Money.of(emi.getCurrencyData(), @@ -192,7 +207,8 @@ public final class RepaymentPeriod { () -> MathUtil.max(getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest() : MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest(), false), getPaidInterest(), false), - () -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest }); + () -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest, totalDisbursedAmount, + emi }); } return dueInterestCalculation.get(); } @@ -307,7 +323,7 @@ public final class RepaymentPeriod { .minus(getDuePrincipal(), mc)// .plus(getPaidPrincipal(), mc);// return MathUtil.negativeToZero(calculatedOutStandingLoanBalance, mc); - }, () -> new Object[] { paidPrincipal, paidInterest, interestPeriods }); + }, () -> new Object[] { paidPrincipal, paidInterest, interestPeriods, totalDisbursedAmount }); } return outstandingBalanceCalculation.get(); } @@ -369,4 +385,22 @@ public final class RepaymentPeriod { this.paidInterest = paidInterest.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; + } } diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java index 552c52c80d..7e8b93f369 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java @@ -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.LoanTransactionProcessingService; 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.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -63,7 +64,8 @@ class LoanScheduleGeneratorTest { void testGenerateLoanSchedule() { LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY, 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(); ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator, @@ -101,7 +103,7 @@ class LoanScheduleGeneratorTest { 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, 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(); ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator, diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index 207da71ab7..8ee3018bb8 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -24,6 +24,8 @@ import java.math.RoundingMode; import java.time.LocalDate; import java.util.ArrayList; 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.ThreadLocalContextUtil; 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.ProgressiveLoanInterestScheduleModel; 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.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -56,6 +60,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.lang.NonNull; +@Slf4j @ExtendWith(MockitoExtension.class) class ProgressiveEMICalculatorTest { @@ -99,6 +104,9 @@ class ProgressiveEMICalculatorTest { public void setupTestDefaults() { Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); 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, @@ -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) { final Money zeroAmount = Money.zero(currency); return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 12cab7ea57..7a58ae8b8e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -232,7 +232,7 @@ public class LoanScheduleAssembler { Boolean allowPartialPeriodInterestCalcualtion = this.fromApiJsonHelper .extractBooleanNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, element); if (allowPartialPeriodInterestCalcualtion == null) { - allowPartialPeriodInterestCalcualtion = loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalcualtion(); + allowPartialPeriodInterestCalcualtion = loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation(); } 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, - loanProductRelatedDetail.isAllowPartialPeriodInterestCalcualtion())) { + loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation())) { final boolean newValue = command .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME); changes.put(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, newValue); - loanProductRelatedDetail.setAllowPartialPeriodInterestCalcualtion(newValue); + loanProductRelatedDetail.setAllowPartialPeriodInterestCalculation(newValue); } if (loanProductRelatedDetail.getInterestCalculationPeriodMethod().isDaily()) { - loanProductRelatedDetail.setAllowPartialPeriodInterestCalcualtion(false); + loanProductRelatedDetail.setAllowPartialPeriodInterestCalculation(false); } final String graceOnPrincipalPaymentParamName = "graceOnPrincipalPayment"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index c6fc94d417..83b14dbb5d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -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 -> { - validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); + validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal, loan); }); } public void validateLoanMultiDisbursementDate(final JsonElement element, final DataValidatorBuilder baseDataValidator, 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); final JsonObject topLevelJsonElement = element.getAsJsonObject(); @@ -1732,11 +1738,31 @@ public final class LoanApplicationValidator { baseDataValidator.reset().parameter(LoanApiConstants.disbursementPrincipalParameterName) .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 .fromInt(interestCalculationPeriodType); boolean considerPartialPeriodUpdates = interestCalculationPeriodMethod.isDaily() ? interestCalculationPeriodMethod.isDaily() - : loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalcualtion(); + : loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation(); if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, element)) { final Boolean considerPartialInterestEnabled = this.fromApiJsonHelper @@ -1780,7 +1806,8 @@ public final class LoanApplicationValidator { .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) .failWithCode("not.supported.for.selected.interest.calcualtion.type"); } @@ -2045,7 +2072,7 @@ public final class LoanApplicationValidator { LoanProduct loanProduct = loan.loanProduct(); if (loanProduct.isMultiDisburseLoan()) { - validateLoanMultiDisbursementDate(element, expectedDisbursementDate, principal); + validateLoanMultiDisbursementDate(element, expectedDisbursementDate, principal, loan); final JsonArray disbursementDataArray = this.fromApiJsonHelper .extractJsonArrayNamed(LoanApiConstants.disbursementDataParameterName, element); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java index bc0479b4b7..aefe4f8bfb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java @@ -201,15 +201,15 @@ public class LoanProductRelatedDetailUpdateUtil { } if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, - loanRepaymentScheduleDetail.isAllowPartialPeriodInterestCalcualtion())) { + loanRepaymentScheduleDetail.isAllowPartialPeriodInterestCalculation())) { final boolean newValue = command .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME); actualChanges.put(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, newValue); - loanRepaymentScheduleDetail.setAllowPartialPeriodInterestCalcualtion(newValue); + loanRepaymentScheduleDetail.setAllowPartialPeriodInterestCalculation(newValue); } if (loanRepaymentScheduleDetail.getInterestCalculationPeriodMethod().isDaily()) { - loanRepaymentScheduleDetail.setAllowPartialPeriodInterestCalcualtion(false); + loanRepaymentScheduleDetail.setAllowPartialPeriodInterestCalculation(false); } final String graceOnPrincipalPaymentParamName = "graceOnPrincipalPayment"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index c78483d507..40284de5ca 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -2232,7 +2232,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } private void validateMultiDisbursementData(final JsonCommand command, LocalDate expectedDisbursementDate, - boolean isDisallowExpectedDisbursements) { + boolean isDisallowExpectedDisbursements, Loan loan) { final String json = command.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); - loanApplicationValidator.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); + loanApplicationValidator.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal, loan); if (!dataValidationErrors.isEmpty()) { 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"; throw new LoanMultiDisbursementException(errorMessage); } - validateMultiDisbursementData(command, expectedDisbursementDate, loan.loanProduct().isDisallowExpectedDisbursements()); + validateMultiDisbursementData(command, expectedDisbursementDate, loan.loanProduct().isDisallowExpectedDisbursements(), loan); this.validateForAddAndDeleteTranche(loan); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 43f92b019e..bad51f132a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -786,7 +786,7 @@ public final class LoanProductDataValidator { } } - validateMultiDisburseLoanData(baseDataValidator, element); + validateMultiDisburseLoanData(baseDataValidator, element, null); 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; if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME, element)) { multiDisburseLoan = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME, @@ -1045,14 +1046,34 @@ public final class LoanProductDataValidator { .integerGreaterThanZero(); 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); 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, final LoanProduct loanProduct) { @@ -1868,7 +1889,7 @@ public final class LoanProductDataValidator { } } - validateMultiDisburseLoanData(baseDataValidator, element); + validateMultiDisburseLoanData(baseDataValidator, element, loanProduct); // validateLoanConfigurableAttributes(baseDataValidator,element); @@ -2549,7 +2570,7 @@ public final class LoanProductDataValidator { } } else if (loanProduct != null) { if (!interestCalculationPeriodMethod.isDaily()) { - considerPartialPeriodUpdates = loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalcualtion(); + considerPartialPeriodUpdates = loanProduct.getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation(); } } @@ -2574,7 +2595,7 @@ public final class LoanProductDataValidator { } else if (loanProduct != null) { multiDisburseLoan = loanProduct.isMultiDisburseLoan(); } - if (multiDisburseLoan != null && multiDisburseLoan) { + if (multiDisburseLoan != null && multiDisburseLoan && !isProgressive(element, loanProduct)) { baseDataValidator.reset().parameter(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME) .failWithCode("not.supported.for.selected.interest.calculation.type"); }
