FINERACT-2114: Fix EMI adjustment when Interest Rate change has with Early Repayment Adjust Last case
This commit is contained in:
committed by
Adam Saghy
parent
425085f4eb
commit
75fa740ff6
+16
@@ -198,6 +198,14 @@ public class Money implements Comparable<Money> {
|
||||
return new Money(this.currencyCode, this.currencyDigitsAfterDecimal, this.amount.stripTrailingZeros(), this.inMultiplesOf, this.mc);
|
||||
}
|
||||
|
||||
public Money copy(final BigDecimal amount) {
|
||||
return new Money(this.currencyCode, this.currencyDigitsAfterDecimal, amount.stripTrailingZeros(), this.inMultiplesOf, this.mc);
|
||||
}
|
||||
|
||||
public Money copy(final double amount) {
|
||||
return copy(BigDecimal.valueOf(amount));
|
||||
}
|
||||
|
||||
public Money plus(final Iterable<? extends Money> moniesToAdd) {
|
||||
BigDecimal total = this.amount;
|
||||
for (final Money moneyProvider : moniesToAdd) {
|
||||
@@ -313,6 +321,14 @@ public class Money implements Comparable<Money> {
|
||||
return Money.of(monetaryCurrency(), newAmount, mc);
|
||||
}
|
||||
|
||||
public Money dividedBy(final long valueToDivideBy) {
|
||||
if (valueToDivideBy == 1) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), getMc());
|
||||
return Money.of(monetaryCurrency(), newAmount, getMc());
|
||||
}
|
||||
|
||||
public Money multipliedBy(final BigDecimal valueToMultiplyBy) {
|
||||
return multipliedBy(valueToMultiplyBy, getMc());
|
||||
}
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.util.List;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
public record EmiAdjustment(//
|
||||
Money originalEmi, //
|
||||
Money emiDifference, //
|
||||
List<RepaymentPeriod> relatedRepaymentPeriods, //
|
||||
long uncountablePeriods//
|
||||
) {
|
||||
|
||||
public boolean shouldBeAdjusted() {
|
||||
double lowerHalfOfRelatedPeriods = Math.floor(numberOfRelatedPeriods() / 2.0);
|
||||
return lowerHalfOfRelatedPeriods > 0.0 && !emiDifference.isZero() && emiDifference.abs() //
|
||||
.multipliedBy(100) //
|
||||
.isGreaterThan(originalEmi.copy(lowerHalfOfRelatedPeriods)); //
|
||||
}
|
||||
|
||||
public Money adjustment() {
|
||||
return emiDifference.dividedBy(Math.max(1, numberOfRelatedPeriods() - uncountablePeriods));
|
||||
}
|
||||
|
||||
public Money adjustedEmi() {
|
||||
return originalEmi.plus(adjustment());
|
||||
}
|
||||
|
||||
public boolean hasLessEmiDifference(EmiAdjustment previousAdjustment) {
|
||||
return emiDifference.abs().isLessThan(previousAdjustment.emiDifference.abs());
|
||||
}
|
||||
|
||||
public boolean hasUncountablePeriods() {
|
||||
return uncountablePeriods > 0;
|
||||
}
|
||||
|
||||
private int numberOfRelatedPeriods() {
|
||||
return relatedRepaymentPeriods.size();
|
||||
}
|
||||
}
|
||||
+23
-35
@@ -34,6 +34,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
|
||||
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.EmiAdjustment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
@@ -287,33 +288,16 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
final List<RepaymentPeriod> relatedRepaymentPeriods) {
|
||||
MathContext mc = scheduleModel.mc();
|
||||
ProgressiveLoanInterestScheduleModel newScheduleModel = null;
|
||||
|
||||
final int numberOfRelatedPeriods = relatedRepaymentPeriods.size();
|
||||
double lowerHalfOfRelatedPeriods = Math.floor(numberOfRelatedPeriods / 2.0);
|
||||
if (lowerHalfOfRelatedPeriods == 0.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long uncountablePeriods;
|
||||
int adjustCounter = 0;
|
||||
EmiAdjustment emiAdjustment;
|
||||
|
||||
do {
|
||||
final Money emiDifference = getDifferenceBetweenLastTwoPeriod(relatedRepaymentPeriods, scheduleModel);
|
||||
if (emiDifference.isZero(mc)) {
|
||||
emiAdjustment = getEmiAdjustment(relatedRepaymentPeriods);
|
||||
if (!emiAdjustment.shouldBeAdjusted()) {
|
||||
break;
|
||||
}
|
||||
final Money originalEmi = relatedRepaymentPeriods.get(numberOfRelatedPeriods - 2).getEmi();
|
||||
boolean shouldBeAdjusted = emiDifference.abs(mc).multipliedBy(100, mc)
|
||||
.isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods), mc));
|
||||
if (!shouldBeAdjusted) {
|
||||
break;
|
||||
}
|
||||
|
||||
uncountablePeriods = relatedRepaymentPeriods.stream().filter(rp -> originalEmi.isLessThan(rp.getTotalPaidAmount())).count();
|
||||
Money adjustment = emiDifference.dividedBy(Math.max(1, numberOfRelatedPeriods - uncountablePeriods), mc);
|
||||
Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel,
|
||||
originalEmi.plus(adjustment, mc));
|
||||
if (adjustedEqualMonthlyInstallmentValue.isEqualTo(originalEmi)) {
|
||||
Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel, emiAdjustment.adjustedEmi());
|
||||
if (adjustedEqualMonthlyInstallmentValue.isEqualTo(emiAdjustment.originalEmi())) {
|
||||
break;
|
||||
}
|
||||
if (newScheduleModel == null) {
|
||||
@@ -329,9 +313,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
});
|
||||
calculateOutstandingBalance(newScheduleModel);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(newScheduleModel);
|
||||
final Money newEmiDifference = getDifferenceBetweenLastTwoPeriod(newScheduleModel.repaymentPeriods(), scheduleModel);
|
||||
final boolean newEmiHasLessDifference = newEmiDifference.abs(mc).isLessThan(emiDifference.abs(mc));
|
||||
if (!newEmiHasLessDifference) {
|
||||
if (!getEmiAdjustment(newScheduleModel.repaymentPeriods()).hasLessEmiDifference(emiAdjustment)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -349,7 +331,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
});
|
||||
calculateOutstandingBalance(scheduleModel);
|
||||
adjustCounter++;
|
||||
} while (uncountablePeriods > 0 && adjustCounter < 3);
|
||||
} while (emiAdjustment.hasUncountablePeriods() && adjustCounter < 3);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -500,16 +482,17 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
: equalMonthlyInstallment;
|
||||
}
|
||||
|
||||
Money getDifferenceBetweenLastTwoPeriod(final List<RepaymentPeriod> repaymentPeriods,
|
||||
final ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
MathContext mc = scheduleModel.mc();
|
||||
int numberOfUpcomingPeriods = repaymentPeriods.size();
|
||||
if (numberOfUpcomingPeriods < 2) {
|
||||
return Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency(), mc);
|
||||
public EmiAdjustment getEmiAdjustment(final List<RepaymentPeriod> repaymentPeriods) {
|
||||
for (int idx = repaymentPeriods.size() - 1; idx > 0; --idx) {
|
||||
RepaymentPeriod lastPeriod = repaymentPeriods.get(idx);
|
||||
RepaymentPeriod penultimatePeriod = repaymentPeriods.get(idx - 1);
|
||||
if (!lastPeriod.isFullyPaid() && !penultimatePeriod.isFullyPaid()) {
|
||||
Money emiDifference = lastPeriod.getEmi().minus(penultimatePeriod.getEmi());
|
||||
return new EmiAdjustment(penultimatePeriod.getEmi(), emiDifference, repaymentPeriods,
|
||||
getUncountablePeriods(repaymentPeriods, penultimatePeriod.getEmi()));
|
||||
}
|
||||
}
|
||||
final RepaymentPeriod lastPeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 1);
|
||||
final RepaymentPeriod penultimatePeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 2);
|
||||
return lastPeriod.getEmi().minus(penultimatePeriod.getEmi(), mc);
|
||||
return new EmiAdjustment(repaymentPeriods.get(0).getEmi(), repaymentPeriods.get(0).getEmi().copy(0.0), repaymentPeriods, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -734,4 +717,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
.reduce(scheduleModel.getZero(), Money::add); //
|
||||
}
|
||||
|
||||
private long getUncountablePeriods(final List<RepaymentPeriod> relatedRepaymentPeriods, final Money originalEmi) {
|
||||
return relatedRepaymentPeriods.stream() //
|
||||
.filter(repaymentPeriod -> originalEmi.isLessThan(repaymentPeriod.getTotalPaidAmount())) //
|
||||
.count(); //
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -480,6 +480,53 @@ class ProgressiveEMICalculatorTest {
|
||||
checkPeriod(interestModel, 5, 0, 16.85, 0.003333333333, 0.06, 16.79, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_reschedule_interest_on0120_adjsLst_dsbAmt100_dayInYears360_daysInMonth30_rpEvery1M() {
|
||||
|
||||
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 = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).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 interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
emiCalculator.payPrincipal(interestModel, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 1, 15), toMoney(17.01));
|
||||
|
||||
final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0);
|
||||
final LocalDate interestChangeDate = LocalDate.of(2024, 1, 20);
|
||||
emiCalculator.changeInterestRate(interestModel, interestChangeDate, interestRateNewValue);
|
||||
|
||||
checkPeriod(interestModel, 0, 0, 16.80, 0.0, 0.0, 0.44, 16.36, 66.63);
|
||||
checkPeriod(interestModel, 0, 1, 16.80, 0.002634408602, 0.26, 0.44, 16.36, 66.63);
|
||||
checkPeriod(interestModel, 0, 2, 16.80, 0.000752688172, 0.06, 0.44, 16.36, 66.63);
|
||||
checkPeriod(interestModel, 0, 3, 16.80, 0.001397849462, 0.12, 0.44, 16.36, 66.63);
|
||||
checkPeriod(interestModel, 1, 0, 16.80, 0.003333333333, 0.22, 16.58, 50.05);
|
||||
checkPeriod(interestModel, 2, 0, 16.80, 0.003333333333, 0.17, 16.63, 33.42);
|
||||
checkPeriod(interestModel, 3, 0, 16.80, 0.003333333333, 0.11, 16.69, 16.73);
|
||||
checkPeriod(interestModel, 4, 0, 16.79, 0.003333333333, 0.06, 16.73, 0.0);
|
||||
checkPeriod(interestModel, 5, 0, 17.01, 0.003333333333, 0.0, 17.01, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user