FINERACT-2114: Interest rate modification
This commit is contained in:
committed by
Adam Saghy
parent
80f4627623
commit
1286b1e72f
-4
@@ -1890,8 +1890,4 @@ public final class LoanApplicationTerms {
|
||||
this.variationDays += daysToAdd;
|
||||
}
|
||||
|
||||
public LocalDate getLoanEndDate() {
|
||||
return loanEndDate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-1
@@ -331,7 +331,10 @@ public class LoanRescheduleRequestDataValidator {
|
||||
}
|
||||
|
||||
if (rescheduleFromDate != null) {
|
||||
installment = loan.getRepaymentScheduleInstallment(rescheduleFromDate);
|
||||
final boolean isProgressiveLoanSchedule = loan.getLoanProductRelatedDetail()
|
||||
.getLoanScheduleType() == LoanScheduleType.PROGRESSIVE;
|
||||
installment = isProgressiveLoanSchedule ? loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate)
|
||||
: loan.getRepaymentScheduleInstallment(rescheduleFromDate);
|
||||
|
||||
if (installment == null) {
|
||||
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode(
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record ProgressiveLoanInterestRate(LocalDate effectiveFrom, LocalDate validFrom,
|
||||
BigDecimal interestRate) implements Comparable<ProgressiveLoanInterestRate> {
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull ProgressiveLoanInterestRate o) {
|
||||
return this.effectiveFrom().compareTo(o.effectiveFrom());
|
||||
}
|
||||
}
|
||||
+29
-2
@@ -18,13 +18,40 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
|
||||
public record ProgressiveLoanInterestScheduleModel(List<ProgressiveLoanInterestRepaymentModel> repayments,
|
||||
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) {
|
||||
public record ProgressiveLoanInterestScheduleModel(List<ProgressiveLoanInterestRepaymentModel> repayments, //
|
||||
List<ProgressiveLoanInterestRate> interestRates, //
|
||||
LoanProductRelatedDetail loanProductRelatedDetail, //
|
||||
Integer installmentAmountInMultiplesOf, //
|
||||
MathContext mc) {
|
||||
|
||||
public ProgressiveLoanInterestScheduleModel(List<ProgressiveLoanInterestRepaymentModel> repayments,
|
||||
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) {
|
||||
this(repayments, new ArrayList<>(1), loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
}
|
||||
|
||||
public void addInterestRate(final LocalDate newInterestDueDate, final BigDecimal newInterestRate) {
|
||||
interestRates.add(new ProgressiveLoanInterestRate(newInterestDueDate, newInterestDueDate.plusDays(1), newInterestRate));
|
||||
interestRates.sort(Collections.reverseOrder());
|
||||
}
|
||||
|
||||
public BigDecimal getInterestRate(final LocalDate effectiveDate) {
|
||||
return interestRates.isEmpty() ? loanProductRelatedDetail.getNominalInterestRatePerPeriod() : findInterestRate(effectiveDate);
|
||||
}
|
||||
|
||||
private BigDecimal findInterestRate(final LocalDate effectiveDate) {
|
||||
return interestRates.stream().filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate))
|
||||
.map(ProgressiveLoanInterestRate::interestRate).findFirst()
|
||||
.orElse(loanProductRelatedDetail.getNominalInterestRatePerPeriod());
|
||||
}
|
||||
|
||||
public int getLoanTermInDays() {
|
||||
if (repayments.isEmpty()) {
|
||||
|
||||
+9
-1
@@ -75,7 +75,6 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
if (lastDueDateVariation != null) {
|
||||
loanEndDate = lastDueDateVariation.getDateValue();
|
||||
}
|
||||
loanApplicationTerms.updateLoanEndDate(loanEndDate);
|
||||
|
||||
// determine the total charges due at time of disbursement
|
||||
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
|
||||
@@ -111,6 +110,15 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
processDisbursements(loanApplicationTerms, scheduleParams, interestScheduleModel, periods, chargesDueAtTimeOfDisbursement);
|
||||
repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber());
|
||||
|
||||
for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) {
|
||||
final LocalDate interestRateChangeEffectiveDate = interestRateChange.getTermVariationApplicableFrom().minusDays(1);
|
||||
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
|
||||
if (interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getFromDate())
|
||||
&& !interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getDueDate())) {
|
||||
emiCalculator.changeInterestRate(interestScheduleModel, interestRateChangeEffectiveDate, newInterestRate);
|
||||
}
|
||||
}
|
||||
|
||||
emiCalculator.findInterestRepaymentPeriod(interestScheduleModel, repaymentPeriod.getDueDate())
|
||||
.ifPresent(interestRepaymentPeriod -> {
|
||||
final Money principalDue = interestRepaymentPeriod.getPrincipalDue();
|
||||
|
||||
+4
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanproduct.calc;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
@@ -42,6 +43,9 @@ public interface EMICalculator {
|
||||
|
||||
void addDisbursement(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate disbursementDueDate, Money disbursedAmount);
|
||||
|
||||
void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestEffectiveDate,
|
||||
BigDecimal newInterestRate);
|
||||
|
||||
ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel);
|
||||
|
||||
ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
|
||||
+58
-1
@@ -97,6 +97,27 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
Optional<ProgressiveLoanInterestRepaymentInterestPeriod> findInterestPeriodForInterestChange(
|
||||
final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate interestRateChangeEffectiveDate) {
|
||||
if (repaymentPeriod == null || interestRateChangeEffectiveDate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return repaymentPeriod.getInterestPeriods().stream()//
|
||||
.filter(interestPeriod -> interestRateChangeEffectiveDate.isEqual(interestPeriod.getFromDate()))//
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
Optional<ProgressiveLoanInterestRepaymentModel> findInterestRepaymentPeriodForInterestChange(
|
||||
final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate interestChangeEffectiveDate) {
|
||||
if (scheduleModel == null || interestChangeEffectiveDate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return scheduleModel.repayments().stream()//
|
||||
.filter(repaymentPeriod -> !interestChangeEffectiveDate.isBefore(repaymentPeriod.getFromDate())
|
||||
&& interestChangeEffectiveDate.isBefore(repaymentPeriod.getDueDate()))//
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add disbursement to Interest Period
|
||||
*/
|
||||
@@ -154,6 +175,42 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
return new ProgressiveLoanInterestScheduleModel(repayments, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestEffectiveDate,
|
||||
final BigDecimal newInterestRate) {
|
||||
final ProgressiveLoanInterestRepaymentModel repaymentPeriod = findInterestRepaymentPeriodForInterestChange(scheduleModel,
|
||||
newInterestEffectiveDate).orElse(null);
|
||||
if (repaymentPeriod == null) {
|
||||
return;
|
||||
}
|
||||
scheduleModel.addInterestRate(newInterestEffectiveDate, newInterestRate);
|
||||
var interestPeriodOptional = findInterestPeriodForInterestChange(repaymentPeriod, newInterestEffectiveDate);
|
||||
if (interestPeriodOptional.isEmpty()) {
|
||||
insertInterestPeriod(scheduleModel, repaymentPeriod, newInterestEffectiveDate);
|
||||
}
|
||||
|
||||
calculateEMIValueAndRateFactors(repaymentPeriod.getDueDate(), scheduleModel);
|
||||
}
|
||||
|
||||
void insertInterestPeriod(final ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate interestChangeDueDate) {
|
||||
// period start date
|
||||
final ProgressiveLoanInterestRepaymentInterestPeriod previousInterestPeriod = repaymentPeriod.getInterestPeriods().stream()
|
||||
.filter(interestPeriod -> interestChangeDueDate.isAfter(interestPeriod.getFromDate())
|
||||
&& interestChangeDueDate.isBefore(interestPeriod.getDueDate()))//
|
||||
.findFirst()//
|
||||
.get();//
|
||||
|
||||
final Money zeroAmount = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
|
||||
final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(interestChangeDueDate,
|
||||
previousInterestPeriod.getDueDate(), BigDecimal.ZERO, zeroAmount, zeroAmount);
|
||||
|
||||
previousInterestPeriod.setDueDate(interestChangeDueDate);
|
||||
|
||||
repaymentPeriod.getInterestPeriods().add(interestPeriod);
|
||||
Collections.sort(repaymentPeriod.getInterestPeriods());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest
|
||||
*/
|
||||
@@ -259,7 +316,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
final ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod, final ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
final MathContext mc = scheduleModel.mc();
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
|
||||
final BigDecimal interestRate = calcNominalInterestRatePercentage(loanProductRelatedDetail.getNominalInterestRatePerPeriod(), mc);
|
||||
final BigDecimal interestRate = calcNominalInterestRatePercentage(scheduleModel.getInterestRate(interestPeriod.getFromDate()), mc);
|
||||
final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
|
||||
final DaysInMonthType daysInMonthType = DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType());
|
||||
final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
|
||||
|
||||
+91
@@ -25,6 +25,7 @@ import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
@@ -50,6 +51,7 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(null);
|
||||
|
||||
private static MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class);
|
||||
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
|
||||
private static LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
|
||||
|
||||
@@ -241,6 +243,95 @@ class ProgressiveEMICalculatorTest {
|
||||
checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule_interest_on0201_4per() {
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
|
||||
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
|
||||
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
|
||||
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
|
||||
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = new BigDecimal("7");
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
final BigDecimal interestRateNewValue = new BigDecimal("4");
|
||||
final LocalDate interestChangeDate = LocalDate.of(2024, 2, 2);
|
||||
final LocalDate interestEffectiveDate = interestChangeDate.minusDays(1);
|
||||
emiCalculator.changeInterestRate(interestSchedule, interestEffectiveDate, interestRateNewValue);
|
||||
|
||||
checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 16.88, 0.003333333333, 0.28, 16.60, 66.97);
|
||||
checkPeriod(interestSchedule, 2, 0, 16.88, 0.003333333333, 0.22, 16.66, 50.31);
|
||||
checkPeriod(interestSchedule, 3, 0, 16.88, 0.003333333333, 0.17, 16.71, 33.60);
|
||||
checkPeriod(interestSchedule, 4, 0, 16.88, 0.003333333333, 0.11, 16.77, 16.83);
|
||||
checkPeriod(interestSchedule, 5, 0, 16.89, 0.003333333333, 0.06, 16.83, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule_interest_on0215_4per() {
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
|
||||
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
|
||||
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
|
||||
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
|
||||
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = new BigDecimal("7");
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
final BigDecimal interestRateNewValue = new BigDecimal("4");
|
||||
final LocalDate interestChangeDate = LocalDate.of(2024, 2, 15);
|
||||
final LocalDate interestEffectiveDate = interestChangeDate.minusDays(1);
|
||||
emiCalculator.changeInterestRate(interestSchedule, interestEffectiveDate, interestRateNewValue);
|
||||
|
||||
checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 16.90, 0.002614942529, 0.22, 0.37, 16.53, 67.04);
|
||||
checkPeriod(interestSchedule, 1, 1, 16.90, 0.001839080460, 0.15, 0.37, 16.53, 67.04);
|
||||
checkPeriod(interestSchedule, 2, 0, 16.90, 0.003333333333, 0.22, 16.68, 50.36);
|
||||
checkPeriod(interestSchedule, 3, 0, 16.90, 0.003333333333, 0.17, 16.73, 33.63);
|
||||
checkPeriod(interestSchedule, 4, 0, 16.90, 0.003333333333, 0.11, 16.79, 16.84);
|
||||
checkPeriod(interestSchedule, 5, 0, 16.90, 0.003333333333, 0.06, 16.84, 0.0);
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month_reschedule() {
|
||||
// final MathContext mc = MoneyHelper.getMathContext();
|
||||
|
||||
Reference in New Issue
Block a user