FINERACT-2421: Fix phantom interest from overdue balance corrections on model
This commit is contained in:
committed by
Adam Saghy
parent
e2fa9e9762
commit
e65257835b
+32
-3
@@ -368,6 +368,18 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
});
|
||||
}
|
||||
|
||||
public void addOverdueBalanceCorrection(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate correctionDate,
|
||||
final Money overdueAmount) {
|
||||
scheduleModel
|
||||
.changeOutstandingBalanceAndUpdateInterestPeriods(correctionDate, scheduleModel.zero(), overdueAmount, scheduleModel.zero())
|
||||
.ifPresent(repaymentPeriod -> {
|
||||
scheduleModel.recordOverdueCorrection(correctionDate, overdueAmount, repaymentPeriod.getDueDate());
|
||||
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
||||
calculateOutstandingBalance(scheduleModel);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, correctionDate);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodFromDate,
|
||||
LocalDate repaymentPeriodDueDate, LocalDate transactionDate, Money interestAmount) {
|
||||
@@ -919,9 +931,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
|
||||
if (!currentDate.equals(model.lastOverdueBalanceChange())) {
|
||||
if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) {
|
||||
addBalanceCorrection(model, fromDate, overduePrincipal);
|
||||
addOverdueBalanceCorrection(model, fromDate, overduePrincipal);
|
||||
} else {
|
||||
addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
|
||||
addOverdueBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
|
||||
}
|
||||
|
||||
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
|
||||
@@ -931,7 +943,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
} else {
|
||||
lastOverdueBalanceChange = currentDate;
|
||||
}
|
||||
addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
|
||||
addOverdueBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
|
||||
model.lastOverdueBalanceChange(lastOverdueBalanceChange);
|
||||
}
|
||||
return true;
|
||||
@@ -1125,7 +1137,24 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
RepaymentPeriod repaymentPeriod, LocalDate tillDate) {
|
||||
MathContext mc = scheduleModel.mc();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
|
||||
|
||||
// Reverse overdue corrections on the copy when they exist ON the target period
|
||||
// but NOT beyond it. Beyond-target corrections provide legitimate cascading interest.
|
||||
// Without reversal, overdue-inflated IPs create phantom futureUnrecognizedInterest.
|
||||
final boolean hasOverdueCorrectionsBeyondTarget = scheduleModelCopy.hasOverdueCorrectionsBeyondDate(repaymentPeriod.getDueDate());
|
||||
final boolean hasOverdueCorrectionsOnTarget = scheduleModelCopy.hasOverdueCorrectionsOnDate(repaymentPeriod.getDueDate());
|
||||
final boolean shouldResetOverdue = !hasOverdueCorrectionsBeyondTarget && hasOverdueCorrectionsOnTarget;
|
||||
|
||||
if (shouldResetOverdue) {
|
||||
scheduleModelCopy.reverseOverdueCorrections();
|
||||
}
|
||||
|
||||
calculateRateFactorForScheduleTillDateInclusive(scheduleModelCopy, tillDate);
|
||||
|
||||
if (shouldResetOverdue) {
|
||||
calculateOutstandingBalance(scheduleModelCopy);
|
||||
}
|
||||
|
||||
Optional<RepaymentPeriod> futureUnrecognizedInterestPeriod = getPeriodWithUnrecognizedInterest(repaymentPeriod, scheduleModelCopy);
|
||||
|
||||
futureUnrecognizedInterestPeriod.ifPresent(period -> {
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.loanproduct.calc.data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
/**
|
||||
* Records an overdue balance correction applied to the schedule model. Used for detecting and reversing overdue
|
||||
* corrections on model copies to prevent phantom futureUnrecognizedInterest.
|
||||
*
|
||||
* @param correctionDate
|
||||
* the date where the correction was applied (matches an InterestPeriod's dueDate after IP split)
|
||||
* @param amount
|
||||
* the correction amount (+X for inflation, -X for deflation)
|
||||
* @param affectedRpDueDate
|
||||
* the dueDate of the RepaymentPeriod that received this correction (for beyond/onTarget detection)
|
||||
*/
|
||||
public record OverdueBalanceCorrection(LocalDate correctionDate, Money amount, LocalDate affectedRpDueDate) {
|
||||
}
|
||||
+30
-3
@@ -68,6 +68,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
|
||||
@Setter
|
||||
private LocalDate lastOverdueBalanceChange;
|
||||
private List<OverdueBalanceCorrection> overdueCorrections = new ArrayList<>();
|
||||
|
||||
public ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repaymentPeriods,
|
||||
final ILoanConfigurationDetails loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
|
||||
@@ -95,9 +96,35 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
loanProductRelatedDetail.isInterestRecalculationEnabled()));
|
||||
}
|
||||
|
||||
public ProgressiveLoanInterestScheduleModel deepCopy(MathContext mc) {
|
||||
return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates, loanProductRelatedDetail,
|
||||
installmentAmountInMultiplesOf, mc, false);
|
||||
public void recordOverdueCorrection(final LocalDate correctionDate, final Money amount, final LocalDate affectedRpDueDate) {
|
||||
overdueCorrections.add(new OverdueBalanceCorrection(correctionDate, amount, affectedRpDueDate));
|
||||
}
|
||||
|
||||
public boolean hasOverdueCorrectionsBeyondDate(final LocalDate targetDueDate) {
|
||||
return overdueCorrections.stream().anyMatch(oc -> oc.affectedRpDueDate().isAfter(targetDueDate));
|
||||
}
|
||||
|
||||
public boolean hasOverdueCorrectionsOnDate(final LocalDate targetDueDate) {
|
||||
return overdueCorrections.stream().anyMatch(oc -> oc.affectedRpDueDate().isEqual(targetDueDate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses all recorded overdue corrections on this model by subtracting each correction's amount from the
|
||||
* corresponding InterestPeriod's balanceCorrectionAmount.
|
||||
*/
|
||||
public void reverseOverdueCorrections() {
|
||||
for (final OverdueBalanceCorrection oc : overdueCorrections) {
|
||||
changeOutstandingBalanceAndUpdateInterestPeriods(oc.correctionDate(), zero(), oc.amount().negated(), zero());
|
||||
}
|
||||
overdueCorrections.clear();
|
||||
this.lastOverdueBalanceChange = null;
|
||||
}
|
||||
|
||||
public ProgressiveLoanInterestScheduleModel deepCopy(final MathContext mc) {
|
||||
final ProgressiveLoanInterestScheduleModel copy = new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc, false);
|
||||
copy.overdueCorrections = new ArrayList<>(this.overdueCorrections);
|
||||
return copy;
|
||||
}
|
||||
|
||||
public ProgressiveLoanInterestScheduleModel copyWithoutPaidAmounts() {
|
||||
|
||||
Reference in New Issue
Block a user