Skip to content

Commit

Permalink
FINERACT-1981: Fix interest calculation when applies multi disbursement
Browse files Browse the repository at this point in the history
  • Loading branch information
janez89 committed Jan 11, 2025
1 parent a919254 commit 5fe7ef6
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import lombok.Data;
import lombok.experimental.Accessors;
Expand Down Expand Up @@ -62,7 +65,8 @@ private ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repayme
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf,
final MathContext mc) {
this.mc = mc;
this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods);
this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods,
(previousPeriod, repaymentPeriod) -> new RepaymentPeriod(previousPeriod, repaymentPeriod, mc));
this.interestRates = new TreeSet<>(interestRates);
this.loanProductRelatedDetail = loanProductRelatedDetail;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
Expand All @@ -74,11 +78,20 @@ public ProgressiveLoanInterestScheduleModel deepCopy(MathContext mc) {
installmentAmountInMultiplesOf, mc);
}

private List<RepaymentPeriod> copyRepaymentPeriods(final List<RepaymentPeriod> repaymentPeriods) {
public ProgressiveLoanInterestScheduleModel emptyCopy() {
final List<RepaymentPeriod> repaymentPeriodCopies = copyRepaymentPeriods(repaymentPeriods,
(previousPeriod, repaymentPeriod) -> new RepaymentPeriod(previousPeriod, repaymentPeriod.getFromDate(),
repaymentPeriod.getDueDate(), repaymentPeriod.getEmi().zero(), mc));
return new ProgressiveLoanInterestScheduleModel(repaymentPeriodCopies, interestRates, loanProductRelatedDetail,
installmentAmountInMultiplesOf, mc);
}

private List<RepaymentPeriod> copyRepaymentPeriods(final List<RepaymentPeriod> repaymentPeriods,
final BiFunction<RepaymentPeriod, RepaymentPeriod, RepaymentPeriod> repaymentCopyFunction) {
final List<RepaymentPeriod> repaymentCopies = new ArrayList<>(repaymentPeriods.size());
RepaymentPeriod previousPeriod = null;
for (RepaymentPeriod repaymentPeriod : repaymentPeriods) {
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, repaymentPeriod, mc);
RepaymentPeriod currentPeriod = repaymentCopyFunction.apply(previousPeriod, repaymentPeriod);
previousPeriod = currentPeriod;
repaymentCopies.add(currentPeriod);
}
Expand Down Expand Up @@ -229,4 +242,40 @@ public Optional<RepaymentPeriod> findRepaymentPeriod(@NotNull LocalDate transact
.filter(period -> isInPeriod(transactionDate, period.getFromDate(), period.getDueDate(), period.isFirstRepaymentPeriod()))//
.findFirst();
}

public boolean isEmpty() {
return repaymentPeriods.stream() //
.filter(rp -> !rp.getEmi().isZero()) //
.findFirst() //
.isEmpty(); //
}

/**
* This method gives you repayment pairs to copy attributes.
*
* @param periodFromDueDate
* Copy from this due periods.
* @param copyFromPeriods
* Copy source
* @param copyConsumer
* Consumer to copy attributes. Params: (from, to)
*/
public void copyPeriodsFrom(final LocalDate periodFromDueDate, List<RepaymentPeriod> copyFromPeriods,
BiConsumer<RepaymentPeriod, RepaymentPeriod> copyConsumer) {
if (copyFromPeriods.isEmpty()) {
return;
}
final Iterator<RepaymentPeriod> actualIterator = repaymentPeriods.iterator();
final Iterator<RepaymentPeriod> copyFromIterator = copyFromPeriods.iterator();
while (actualIterator.hasNext()) {
final RepaymentPeriod copyFromPeriod = copyFromIterator.next();
RepaymentPeriod actualPeriod = actualIterator.next();
while (actualIterator.hasNext() && !copyFromPeriod.getDueDate().isEqual(actualPeriod.getDueDate())) {
actualPeriod = actualIterator.next();
}
if (!actualPeriod.getDueDate().isBefore(periodFromDueDate)) {
copyConsumer.accept(copyFromPeriod, actualPeriod);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,17 @@ public Optional<RepaymentPeriod> findRepaymentPeriod(final ProgressiveLoanIntere
@Override
public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate,
final Money disbursedAmount) {
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount, scheduleModel.zero())
addDisbursement(scheduleModel,
new ProgressiveOperation(ProgressiveOperation.Action.DISBURSEMENT, disbursementDueDate, disbursedAmount));
}

void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final ProgressiveOperation operation) {
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
scheduleModel.zero())
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, disbursementDueDate), scheduleModel));
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
operation));
}

private LocalDate getEffectiveRepaymentDueDate(final ProgressiveLoanInterestScheduleModel scheduleModel,
Expand All @@ -125,13 +133,16 @@ private LocalDate getEffectiveRepaymentDueDate(final ProgressiveLoanInterestSche
@Override
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate,
final BigDecimal newInterestRate) {
final ProgressiveOperation operation = new ProgressiveOperation(ProgressiveOperation.Action.INTEREST_RATE_CHANGE,
newInterestSubmittedOnDate, null);
final LocalDate interestRateChangeEffectiveDate = newInterestSubmittedOnDate.minusDays(1);
scheduleModel.addInterestRate(interestRateChangeEffectiveDate, newInterestRate);
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.zero(),
scheduleModel.zero())
.ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel));
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel,
operation));
}

@Override
Expand Down Expand Up @@ -284,14 +295,23 @@ private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(
* Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest
*/
void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaymentPeriodDueDate,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
final ProgressiveLoanInterestScheduleModel scheduleModel, final ProgressiveOperation operation) {
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
final boolean onlyOnActualModelShouldApply = scheduleModel.isEmpty()
|| operation.getAction() == ProgressiveOperation.Action.INTEREST_RATE_CHANGE;

calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
calculateOutstandingBalance(scheduleModel);
calculateEMIOnPeriods(relatedRepaymentPeriods, scheduleModel);
if (onlyOnActualModelShouldApply) {
calculateEMIOnActualModel(relatedRepaymentPeriods, scheduleModel);
} else {
calculateEMIOnNewEmptyModelAndMerge(relatedRepaymentPeriods, scheduleModel, operation);
}
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, relatedRepaymentPeriods);
if (onlyOnActualModelShouldApply) {
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, relatedRepaymentPeriods);
}
}

private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel scheduleModel) {
Expand Down Expand Up @@ -482,15 +502,15 @@ BigDecimal calculateRateFactorPerPeriodBasedOnRepaymentFrequency(final BigDecima
};
}

void calculateEMIOnPeriods(final List<RepaymentPeriod> repaymentPeriods, final ProgressiveLoanInterestScheduleModel scheduleModel) {
private void calculateEMIOnActualModel(List<RepaymentPeriod> repaymentPeriods, ProgressiveLoanInterestScheduleModel scheduleModel) {
if (repaymentPeriods.isEmpty()) {
return;
}
final MathContext mc = scheduleModel.mc();
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(repaymentPeriods, mc));
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods, mc));
final RepaymentPeriod startPeriod = repaymentPeriods.get(0);
// TODO: double check

final Money outstandingBalance = startPeriod.getInitialBalanceForEmiRecalculation();

final Money equalMonthlyInstallment = Money.of(outstandingBalance.getCurrencyData(),
Expand All @@ -505,6 +525,21 @@ void calculateEMIOnPeriods(final List<RepaymentPeriod> repaymentPeriods, final P
});
}

private void calculateEMIOnNewEmptyModelAndMerge(List<RepaymentPeriod> repaymentPeriods,
ProgressiveLoanInterestScheduleModel scheduleModel, final ProgressiveOperation operation) {
if (repaymentPeriods.isEmpty()) {
return;
}
final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.emptyCopy();
addDisbursement(scheduleModelCopy, operation);

final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate();
scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> {
actualRepaymentPeriod.setEmi(actualRepaymentPeriod.getEmi().plus(newRepaymentPeriod.getEmi()));
actualRepaymentPeriod.setOriginalEmi(actualRepaymentPeriod.getOriginalEmi().plus(newRepaymentPeriod.getOriginalEmi()));
});
}

Money applyInstallmentAmountInMultiplesOf(final ProgressiveLoanInterestScheduleModel scheduleModel,
final Money equalMonthlyInstallment) {
return scheduleModel.installmentAmountInMultiplesOf() != null
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

import java.time.LocalDate;
import lombok.Data;
import org.apache.fineract.organisation.monetary.domain.Money;

@Data
public class ProgressiveOperation {

public enum Action {
DISBURSEMENT, INTEREST_RATE_CHANGE
}

private final Action action;
private final LocalDate submittedOnDate;

private final Money amount;
}
Original file line number Diff line number Diff line change
Expand Up @@ -953,14 +953,14 @@ public void test_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_re
disbursedAmount = toMoney(25.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount);

checkPeriod(interestSchedule, 0, 0, 29.94, 0.001019591398, 0.00, 1.15, 28.79, 146.21);
checkPeriod(interestSchedule, 0, 1, 29.94, 0.000764693548, 0.08, 1.15, 28.79, 146.21);
checkPeriod(interestSchedule, 0, 2, 29.94, 0.006117548387, 1.07, 1.15, 28.79, 146.21);
checkPeriod(interestSchedule, 1, 0, 29.94, 0.007901833333, 1.16, 28.78, 117.43);
checkPeriod(interestSchedule, 2, 0, 29.94, 0.007901833333, 0.93, 29.01, 88.42);
checkPeriod(interestSchedule, 3, 0, 29.94, 0.007901833333, 0.70, 29.24, 59.18);
checkPeriod(interestSchedule, 4, 0, 29.94, 0.007901833333, 0.47, 29.47, 29.71);
checkPeriod(interestSchedule, 5, 0, 29.94, 0.007901833333, 0.23, 29.71, 0.0);
checkPeriod(interestSchedule, 0, 0, 29.93, 0.001019591398, 0.00, 1.15, 28.78, 146.22);
checkPeriod(interestSchedule, 0, 1, 29.93, 0.000764693548, 0.08, 1.15, 28.78, 146.22);
checkPeriod(interestSchedule, 0, 2, 29.93, 0.006117548387, 1.07, 1.15, 28.78, 146.22);
checkPeriod(interestSchedule, 1, 0, 29.93, 0.007901833333, 1.16, 28.77, 117.45);
checkPeriod(interestSchedule, 2, 0, 29.93, 0.007901833333, 0.93, 29.00, 88.45);
checkPeriod(interestSchedule, 3, 0, 29.93, 0.007901833333, 0.70, 29.23, 59.22);
checkPeriod(interestSchedule, 4, 0, 29.93, 0.007901833333, 0.47, 29.46, 29.76);
checkPeriod(interestSchedule, 5, 0, 30.0, 0.007901833333, 0.24, 29.76, 0.0);
}

@Test
Expand Down Expand Up @@ -1037,10 +1037,10 @@ public void test_multidisbursement_total_repay1st_dayInYears360_daysInMonth30_re
Assertions.assertEquals(0.0, toDouble(interestSchedule.getTotalDueInterest()));

emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), toMoney(200.0));
Assertions.assertEquals(4.20, toDouble(interestSchedule.getTotalDueInterest()));
Assertions.assertEquals(4.11, toDouble(interestSchedule.getTotalDueInterest()));

checkEmi(interestSchedule, 4, 84.03);
checkEmi(interestSchedule, 5, 84.05);
checkEmi(interestSchedule, 4, 85.05);
checkEmi(interestSchedule, 5, 78.86);
}

@Test
Expand Down

0 comments on commit 5fe7ef6

Please sign in to comment.