FINERACT-1981: Adjust EMI if needed

This commit is contained in:
Adam Saghy
2024-07-08 20:33:16 +02:00
parent 5093ca7363
commit 67440c47ab
9 changed files with 390 additions and 54 deletions
@@ -119,8 +119,8 @@ public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanSc
: scheduleParams.getPeriodStartDate();
List<PreGeneratedLoanSchedulePeriod> expectedRepaymentPeriods = getScheduledDateGenerator()
.generateRepaymentPeriods(startDate, loanApplicationTerms, holidayDetailDTO);
emiCalculationResult = getEMICalculator().calculateEMIValueAndRateFactors(scheduleParams,
loanApplicationTerms.toLoanProductRelatedDetail(), expectedRepaymentPeriods, mc);
emiCalculationResult = getEMICalculator().calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams,
expectedRepaymentPeriods, mc);
}
// 5 determine principal,interest of repayment period
@@ -55,12 +55,11 @@ public class ProgressiveLoanScheduleGenerator extends AbstractProgressiveLoanSch
final LoanScheduleParams loanScheduleParams, final EMICalculationResult emiCalculationResult, final MathContext mc) {
final Money equalMonthlyInstallmentValue = loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null
? Money.of(loanApplicationTerms.getCurrency(),
Money.roundToMultiplesOf(emiCalculationResult.getEqualMonthlyInstallmentValue(),
loanApplicationTerms.getInstallmentAmountInMultiplesOf()))
: Money.of(loanApplicationTerms.getCurrency(), emiCalculationResult.getEqualMonthlyInstallmentValue());
? Money.roundToMultiplesOf(emiCalculationResult.getEqualMonthlyInstallmentValue(),
loanApplicationTerms.getInstallmentAmountInMultiplesOf())
: emiCalculationResult.getEqualMonthlyInstallmentValue();
final BigDecimal rateFactorMinus1 = emiCalculationResult.getNextRepaymentPeriodRateFactorMinus1();
final Money calculatedInterest = loanScheduleParams.getOutstandingBalance().multipliedBy(rateFactorMinus1);
final Money calculatedInterest = loanScheduleParams.getOutstandingBalanceAsPerRest().multipliedBy(rateFactorMinus1);
final Money calculatedPrincipal = equalMonthlyInstallmentValue.minus(calculatedInterest);
return new PrincipalInterest(
@@ -72,7 +71,7 @@ public class ProgressiveLoanScheduleGenerator extends AbstractProgressiveLoanSch
final LoanApplicationTerms loanApplicationTerms, final LoanScheduleParams loanScheduleParams) {
final boolean isLastRepaymentPeriod = loanScheduleParams.getPeriodNumber() == loanApplicationTerms.getActualNoOfRepaymnets();
if (isLastRepaymentPeriod) {
final Money remainingAmount = loanScheduleParams.getOutstandingBalance().minus(calculatedPrincipal);
final Money remainingAmount = loanScheduleParams.getOutstandingBalanceAsPerRest().minus(calculatedPrincipal);
return calculatedPrincipal.plus(remainingAmount);
}
return calculatedPrincipal;
@@ -23,12 +23,13 @@ import java.util.List;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.organisation.monetary.domain.Money;
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class EMICalculationResult {
@Getter
private final BigDecimal equalMonthlyInstallmentValue;
private final Money equalMonthlyInstallmentValue;
private final List<BigDecimal> repaymentPeriodRateFactorMinus1List;
private int counter = 0;
@@ -21,12 +21,11 @@ package org.apache.fineract.portfolio.loanproduct.calc;
import java.math.MathContext;
import java.util.List;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
public interface EMICalculator {
EMICalculationResult calculateEMIValueAndRateFactors(LoanScheduleParams scheduleParams,
LoanProductRelatedDetail loanProductRelatedDetail, List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods,
MathContext mc);
EMICalculationResult calculateEMIValueAndRateFactors(LoanApplicationTerms loanApplicationTerms, LoanScheduleParams scheduleParams,
List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods, MathContext mc);
}
@@ -25,10 +25,12 @@ import java.time.Year;
import java.util.List;
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.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.loanschedule.data.LoanScheduleParams;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.springframework.stereotype.Component;
@@ -42,25 +44,28 @@ public final class ProgressiveEMICalculator implements EMICalculator {
/**
* Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest
*
* @param loanApplicationTerms
* LoanTermApplication
*
* @param scheduleParams
* Loan Schedule Params
*
* @param loanProductRelatedDetail
* Loan Product Related Detail from LoanTermApplication
*
* @param expectedRepaymentPeriods
* Expected Repayment Periods
*
* @param mc
* @return
* MathContext for rounding
*
* @return EMICalculationResult Contains rate factor for each period and calculated EMI
*/
@Override
public EMICalculationResult calculateEMIValueAndRateFactors(final LoanScheduleParams scheduleParams,
final LoanProductRelatedDetail loanProductRelatedDetail, final List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods,
public EMICalculationResult calculateEMIValueAndRateFactors(final LoanApplicationTerms loanApplicationTerms,
final LoanScheduleParams scheduleParams, final List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods,
final MathContext mc) {
final LoanProductRelatedDetail loanProductRelatedDetail = loanApplicationTerms.toLoanProductRelatedDetail();
final BigDecimal nominalInterestRatePerPeriod = calcNominalInterestRatePerPeriod(
loanProductRelatedDetail.getNominalInterestRatePerPeriod(), mc);
final BigDecimal outstandingBalance = scheduleParams.getOutstandingBalanceAsPerRest().getAmount();
final Money outstandingBalance = scheduleParams.getOutstandingBalanceAsPerRest();
final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
final DaysInMonthType daysInMonthType = DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType());
final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
@@ -69,7 +74,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final List<BigDecimal> rateFactorList = getRateFactorList(expectedRepaymentPeriods, nominalInterestRatePerPeriod, daysInYearType,
daysInMonthType, repaymentFrequency, repaymentEvery, mc);
return calculateEMI(rateFactorList, outstandingBalance, mc);
return calculateEMI(loanApplicationTerms, scheduleParams, rateFactorList, outstandingBalance, mc);
}
/**
@@ -200,18 +205,98 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* Calculate EMI parts and return an EMI calculation result object with repayment installment rate factors
*
* @param rateFactorList
* @param principal
* @param outstandingBalanceForRest
* @param mc
* @return
*/
EMICalculationResult calculateEMI(final List<BigDecimal> rateFactorList, final BigDecimal principal, final MathContext mc) {
EMICalculationResult calculateEMI(final LoanApplicationTerms loanApplicationTerms, final LoanScheduleParams loanScheduleParams,
final List<BigDecimal> rateFactorList, final Money outstandingBalanceForRest, final MathContext mc) {
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorN(rateFactorList, mc));
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(rateFactorList, mc));
final BigDecimal emiValue = MathUtil.stripTrailingZeros(calculateEMIValue(rateFactorN, principal, fnResult, mc));
final Money emiValue = Money.of(loanApplicationTerms.getCurrency(),
calculateEMIValue(rateFactorN, outstandingBalanceForRest.getAmount(), fnResult, mc));
final List<BigDecimal> rateFactorMinus1List = getRateFactorMinus1List(rateFactorList, mc);
return new EMICalculationResult(emiValue, rateFactorMinus1List);
final Money adjustedEqualMonthlyInstallmentValue = adjustEMIForMoreStreamlinedRepaymentSchedule(loanApplicationTerms,
loanScheduleParams, emiValue, rateFactorMinus1List, mc);
return new EMICalculationResult(adjustedEqualMonthlyInstallmentValue, rateFactorMinus1List);
}
/**
* Due to rounding or unequal installments, the first calculated EMI might not be the best one! Reiterate with
* adjusted EMI to get a better streamlined repayment schedule (less difference between calculated EMI and last
* installment EMI).
*
* @param loanApplicationTerms
* @param loanScheduleParams
* @param equalMonthlyInstallmentValue
* @param rateFactorMinus1List
* @param mc
* @return
*/
Money adjustEMIForMoreStreamlinedRepaymentSchedule(final LoanApplicationTerms loanApplicationTerms,
final LoanScheduleParams loanScheduleParams, final Money equalMonthlyInstallmentValue, List<BigDecimal> rateFactorMinus1List,
final MathContext mc) {
int numberOfUpcomingPeriods = loanApplicationTerms.getNumberOfRepayments() - loanScheduleParams.getPeriodNumber() + 1;
if (numberOfUpcomingPeriods < 2) {
return equalMonthlyInstallmentValue;
}
RepaymentScheduleModel repaymentScheduleModel = generateRepaymentScheduleModel(loanApplicationTerms, loanScheduleParams,
equalMonthlyInstallmentValue, rateFactorMinus1List);
Money calculatedLastEMI = repaymentScheduleModel.getScheduleList().get(repaymentScheduleModel.getScheduleList().size() - 1).emi();
Money originalDifference = calculatedLastEMI.minus(equalMonthlyInstallmentValue);
if (originalDifference.isZero()) {
return equalMonthlyInstallmentValue;
}
double lowerHalfOfUpcomingPeriods = Math.floor((double) numberOfUpcomingPeriods / 2);
if (lowerHalfOfUpcomingPeriods == 0.0) {
return equalMonthlyInstallmentValue;
}
boolean shouldBeAdjusted = originalDifference.abs().multipliedBy(100)
.isGreaterThan(Money.of(equalMonthlyInstallmentValue.getCurrency(), BigDecimal.valueOf(lowerHalfOfUpcomingPeriods)));
// Reiterate only when needed
if (shouldBeAdjusted) {
Money adjustment = originalDifference.dividedBy(numberOfUpcomingPeriods, mc.getRoundingMode());
Money adjustedEqualMonthlyInstallmentValue = equalMonthlyInstallmentValue.plus(adjustment);
RepaymentScheduleModel repaymentScheduleModelWithAdjustedEMI = generateRepaymentScheduleModel(loanApplicationTerms,
loanScheduleParams, adjustedEqualMonthlyInstallmentValue, rateFactorMinus1List);
Money calculatedLastEMIAfterAdjustment = repaymentScheduleModelWithAdjustedEMI.getScheduleList()
.get(repaymentScheduleModelWithAdjustedEMI.getScheduleList().size() - 1).emi();
Money differenceAfterEMIAdjustment = calculatedLastEMIAfterAdjustment.minus(adjustedEqualMonthlyInstallmentValue);
// Use the adjusted EMI only if it is better than the original one
return differenceAfterEMIAdjustment.abs().isLessThan(originalDifference.abs()) ? adjustedEqualMonthlyInstallmentValue
: equalMonthlyInstallmentValue;
} else {
return equalMonthlyInstallmentValue;
}
}
RepaymentScheduleModel generateRepaymentScheduleModel(LoanApplicationTerms loanApplicationTerms, LoanScheduleParams loanScheduleParams,
Money equalMonthlyInstallmentValue, List<BigDecimal> rateFactorMinus1List) {
RepaymentScheduleModel repaymentScheduleModel = new RepaymentScheduleModel();
Money balanceOfLoan = loanScheduleParams.getOutstandingBalanceAsPerRest();
for (int i = 0; i < loanApplicationTerms.getNumberOfRepayments(); i++) {
final Money calculatedInterest = balanceOfLoan.multipliedBy(rateFactorMinus1List.get(i));
// WE need to calculate EMI differently for last installment (decided by number of repayments or when
// schedule got shorter then planned)
if (balanceOfLoan.isLessThan(equalMonthlyInstallmentValue.minus(calculatedInterest))
|| i == loanApplicationTerms.getNumberOfRepayments() - 1) {
equalMonthlyInstallmentValue = balanceOfLoan.plus(calculatedInterest);
}
final Money calculatedPrincipal = equalMonthlyInstallmentValue.minus(calculatedInterest);
repaymentScheduleModel.addRepaymentPeriodModel(
new RepaymentPeriodModel(balanceOfLoan, equalMonthlyInstallmentValue, calculatedInterest, calculatedPrincipal));
balanceOfLoan = balanceOfLoan.minus(calculatedPrincipal);
// We can stop processing if there is no outstanding principal
if (balanceOfLoan.isZero()) {
break;
}
}
return repaymentScheduleModel;
}
/**
@@ -252,14 +337,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* Calculate the EMI (Equal Monthly Installment) value
*
* @param rateFactorN
* @param principal
* @param outstandingBalanceForRest
* @param fnResult
* @param mc
* @return
*/
BigDecimal calculateEMIValue(final BigDecimal rateFactorN, final BigDecimal principal, final BigDecimal fnResult,
BigDecimal calculateEMIValue(final BigDecimal rateFactorN, final BigDecimal outstandingBalanceForRest, final BigDecimal fnResult,
final MathContext mc) {
return rateFactorN.multiply(principal, mc).divide(fnResult, mc);
return rateFactorN.multiply(outstandingBalanceForRest, mc).divide(fnResult, mc);
}
/**
@@ -0,0 +1,24 @@
/**
* 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 org.apache.fineract.organisation.monetary.domain.Money;
public record RepaymentPeriodModel(Money balanceOfLoan, Money emi, Money principal, Money interest) {
}
@@ -0,0 +1,33 @@
/**
* 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.util.ArrayList;
import java.util.List;
import lombok.Getter;
@Getter
public class RepaymentScheduleModel {
List<RepaymentPeriodModel> scheduleList = new ArrayList<>();
public void addRepaymentPeriodModel(final RepaymentPeriodModel repaymentPeriodModel) {
scheduleList.add(repaymentPeriodModel);
}
}
@@ -35,6 +35,7 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelDisbursementPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PreGeneratedLoanSchedulePeriod;
@@ -55,6 +56,7 @@ class ProgressiveEMICalculatorTest {
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
private static LoanScheduleParams scheduleParams = Mockito.mock(LoanScheduleParams.class);
private static LoanApplicationTerms loanApplicationTerms = Mockito.mock(LoanApplicationTerms.class);
private static LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
private static final MonetaryCurrency monetaryCurrency = MonetaryCurrency
@@ -77,6 +79,8 @@ class ProgressiveEMICalculatorTest {
// When
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
Mockito.when(loanApplicationTerms.toLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
Mockito.when(loanApplicationTerms.getCurrency()).thenReturn(monetaryCurrency);
}
private BigDecimal getRateFactorsByMonth(final DaysInYearType daysInYearType, final DaysInMonthType daysInMonthType,
@@ -166,11 +170,11 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail,
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams,
expectedRepaymentPeriods, mc);
// 17.13
Assertions.assertEquals(BigDecimal.valueOf(17.1280789505), result.getEqualMonthlyInstallmentValue());
Assertions.assertEquals(BigDecimal.valueOf(17.13), result.getEqualMonthlyInstallmentValue().getAmount());
Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), result.getNextRepaymentPeriodRateFactorMinus1());
Assertions.assertEquals(BigDecimal.valueOf(0.00751321858), result.getNextRepaymentPeriodRateFactorMinus1());
@@ -208,11 +212,11 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail,
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams,
expectedRepaymentPeriods, mc);
// 1713.12
Assertions.assertEquals(BigDecimal.valueOf(1713.12436983), result.getEqualMonthlyInstallmentValue());
Assertions.assertEquals(BigDecimal.valueOf(1713.12), result.getEqualMonthlyInstallmentValue().getAmount());
Assertions.assertEquals(BigDecimal.valueOf(0.00804485776), result.getNextRepaymentPeriodRateFactorMinus1());
Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), result.getNextRepaymentPeriodRateFactorMinus1());
@@ -247,11 +251,11 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail,
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams,
expectedRepaymentPeriods, mc);
// 17.13
Assertions.assertEquals(BigDecimal.valueOf(17.1293512777), result.getEqualMonthlyInstallmentValue());
Assertions.assertEquals(BigDecimal.valueOf(17.13), result.getEqualMonthlyInstallmentValue().getAmount());
Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), result.getNextRepaymentPeriodRateFactorMinus1());
Assertions.assertEquals(BigDecimal.valueOf(0.00753380274), result.getNextRepaymentPeriodRateFactorMinus1());
@@ -290,11 +294,11 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail,
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams,
expectedRepaymentPeriods, mc);
// 17.13
Assertions.assertEquals(BigDecimal.valueOf(17.1306301273), result.getEqualMonthlyInstallmentValue());
Assertions.assertEquals(BigDecimal.valueOf(17.13), result.getEqualMonthlyInstallmentValue().getAmount());
Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
@@ -333,11 +337,11 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail,
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams,
expectedRepaymentPeriods, mc);
// 16.77
Assertions.assertEquals(BigDecimal.valueOf(16.7731989919), result.getEqualMonthlyInstallmentValue());
Assertions.assertEquals(BigDecimal.valueOf(16.77), result.getEqualMonthlyInstallmentValue().getAmount());
final BigDecimal fixValue = new BigDecimal("0.0018235");
Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
@@ -377,11 +381,11 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail,
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams,
expectedRepaymentPeriods, mc);
// 16.88
Assertions.assertEquals(new BigDecimal("16.8824319133"), result.getEqualMonthlyInstallmentValue());
Assertions.assertEquals(new BigDecimal("16.88"), result.getEqualMonthlyInstallmentValue().getAmount());
final BigDecimal fixValue = new BigDecimal("0.00368752222");
Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
@@ -421,11 +425,11 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.DAYS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15);
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail,
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams,
expectedRepaymentPeriods, mc);
// 16.90
Assertions.assertEquals(new BigDecimal("16.8978941103"), result.getEqualMonthlyInstallmentValue());
Assertions.assertEquals(new BigDecimal("16.90"), result.getEqualMonthlyInstallmentValue().getAmount());
final BigDecimal fixValue = new BigDecimal("0.00395091667");
Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
@@ -470,11 +474,11 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail,
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams,
expectedRepaymentPeriods, mc);
// 17.13
Assertions.assertEquals(new BigDecimal("15.3574482569"), result.getEqualMonthlyInstallmentValue());
// 15.36
Assertions.assertEquals(new BigDecimal("15.36"), result.getEqualMonthlyInstallmentValue().getAmount());
Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
@@ -510,11 +514,11 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail,
final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams,
expectedRepaymentPeriods, mc);
// 17.13
Assertions.assertEquals(new BigDecimal("250"), result.getEqualMonthlyInstallmentValue());
// 250.00
Assertions.assertEquals(new BigDecimal("250.00"), result.getEqualMonthlyInstallmentValue().getAmount());
Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
@@ -548,7 +552,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
try {
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail, expectedRepaymentPeriods, mc);
emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams, expectedRepaymentPeriods, mc);
Assertions.fail();
} catch (Exception e) {
Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
@@ -574,7 +578,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
try {
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail, expectedRepaymentPeriods, mc);
emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams, expectedRepaymentPeriods, mc);
Assertions.fail();
} catch (Exception e) {
Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
@@ -600,7 +604,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
try {
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, loanProductRelatedDetail, expectedRepaymentPeriods, mc);
emiCalculator.calculateEMIValueAndRateFactors(loanApplicationTerms, scheduleParams, expectedRepaymentPeriods, mc);
Assertions.fail();
} catch (Exception e) {
Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.integrationtests;
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.InterestRateFrequencyType.YEARS;
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.RepaymentFrequencyType.DAYS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -70,8 +72,9 @@ import org.apache.fineract.integrationtests.common.accounting.Account;
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.EarlyPaymentLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor;
@@ -81,11 +84,9 @@ import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ExtendWith(LoanTestLifecycleExtension.class)
public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoanIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(AdvancedPaymentAllocationLoanRepaymentScheduleTest.class);
@@ -4410,6 +4411,196 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
});
}
// UC138: Advanced payment allocation with Interest, EMI is correctly calculated, no adjustment
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Create a Loan product with Adv. Pment. Alloc. and with 5% Interest, 360/30, 1 repayment per month
// 2. Submit Loan and approve
// 3. Disburse
// 4. Validate Repayment Schedule
@Test
public void uc141() {
final String operationDate = "1 January 2024";
runAt(operationDate, () -> {
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(5.0).interestCalculationPeriodType(DAYS).interestRateFrequencyType(YEARS)
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360.getValue())
.numberOfRepayments(5)//
.repaymentEvery(1)//
.repaymentFrequencyType(2L)//
.enableDownPayment(true)//
.allowPartialPeriodInterestCalcualtion(false)//
.enableAutoRepaymentForDownPayment(false)//
.multiDisburseLoan(false)//
.disallowExpectedDisbursements(null)//
.allowApprovedDisbursedAmountsOverApplied(null)//
.overAppliedCalculationType(null)//
.overAppliedNumber(null)//
.installmentAmountInMultiplesOf(null)//
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25))//
;//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 5);
applicationRequest = applicationRequest.numberOfRepayments(5).loanTermFrequency(5).loanTermFrequencyType(2)
.interestRatePerPeriod(BigDecimal.valueOf(5)).interestCalculationPeriodType(DAYS)
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(1)
.repaymentFrequencyType(2);
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
.approvedLoanAmount(BigDecimal.valueOf(100)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 100.94, 0.0, 100.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 1, 1), 25.0, 0.0, 25.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 2, 1), 14.88, 0.0, 14.88, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.31, 0.0,
0.31, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 3, 1), 14.94, 0.0, 14.94, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0,
0.25, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 4, 1), 15.00, 0.0, 15.00, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.19, 0.0,
0.19, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 5, 1), 15.06, 0.0, 15.06, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.13, 0.0,
0.13, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 6, 1), 15.12, 0.0, 15.12, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.06, 0.0,
0.06, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
assertEquals(loanDetails.getNumberOfRepayments(), 5);
});
}
// UC139: Advanced payment allocation with Interest, originally calculated EMI, need adjustment
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Create a Loan product with Adv. Pment. Alloc. and with 12.3% Interest, 360/30, 1 repayment per month
// 2. Submit Loan and approve
// 3. Disburse
// 4. Validate Repayment Schedule
@Test
public void uc142() {
final String operationDate = "1 January 2024";
runAt(operationDate, () -> {
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(12.3).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360.getValue())
.numberOfRepayments(5)//
.repaymentEvery(1)//
.repaymentFrequencyType(2L)//
.enableDownPayment(true)//
.allowPartialPeriodInterestCalcualtion(false)//
.enableAutoRepaymentForDownPayment(false)//
.multiDisburseLoan(false)//
.disallowExpectedDisbursements(null)//
.allowApprovedDisbursedAmountsOverApplied(null)//
.overAppliedCalculationType(null)//
.overAppliedNumber(null)//
.installmentAmountInMultiplesOf(null)//
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25))//
;//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 5);
applicationRequest = applicationRequest.numberOfRepayments(5).loanTermFrequency(5).loanTermFrequencyType(2)
.interestRatePerPeriod(BigDecimal.valueOf(12.3)).interestCalculationPeriodType(DAYS)
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(1)
.repaymentFrequencyType(2);
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
.approvedLoanAmount(BigDecimal.valueOf(100)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 102.33, 0.0, 100.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 1, 1), 25.0, 0.0, 25.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 2, 1), 14.70, 0.0, 14.70, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.77, 0.0,
0.77, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 3, 1), 14.85, 0.0, 14.85, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.62, 0.0,
0.62, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 4, 1), 15.00, 0.0, 15.00, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.47, 0.0,
0.47, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 5, 1), 15.16, 0.0, 15.16, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.31, 0.0,
0.31, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 6, 1), 15.29, 0.0, 15.29, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.16, 0.0,
0.16, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
assertEquals(loanDetails.getNumberOfRepayments(), 5);
});
}
// UC14-: Advanced payment allocation with Interest, originally calculated EMI, need adjustment, adjusted EMI is not
// better than original, use original EMI
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Create a Loan product with Adv. Pment. Alloc. and with 5% Interest, 360/30, 1 repayment per month
// 2. Submit Loan and approve
// 3. Disburse
// 4. Validate Repayment Schedule
@Test
public void uc143() {
final String operationDate = "1 January 2024";
runAt(operationDate, () -> {
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(12.3).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360.getValue())
.numberOfRepayments(5)//
.repaymentEvery(1)//
.repaymentFrequencyType(2L)//
.enableDownPayment(true)//
.allowPartialPeriodInterestCalcualtion(false)//
.enableAutoRepaymentForDownPayment(false)//
.multiDisburseLoan(false)//
.disallowExpectedDisbursements(null)//
.allowApprovedDisbursedAmountsOverApplied(null)//
.overAppliedCalculationType(null)//
.overAppliedNumber(null)//
.installmentAmountInMultiplesOf(null)//
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25))//
;//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 5);
applicationRequest = applicationRequest.numberOfRepayments(5).loanTermFrequency(5).loanTermFrequencyType(2)
.interestRatePerPeriod(BigDecimal.valueOf(12.3)).interestCalculationPeriodType(DAYS)
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(1)
.repaymentFrequencyType(2);
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
.approvedLoanAmount(BigDecimal.valueOf(100)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 102.33, 0.0, 100.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 1, 1), 25.0, 0.0, 25.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 2, 1), 14.70, 0.0, 14.70, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.77, 0.0,
0.77, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 3, 1), 14.85, 0.0, 14.85, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.62, 0.0,
0.62, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 4, 1), 15.00, 0.0, 15.00, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.47, 0.0,
0.47, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 5, 1), 15.16, 0.0, 15.16, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.31, 0.0,
0.31, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 6, 1), 15.29, 0.0, 15.29, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.16, 0.0,
0.16, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
assertEquals(loanDetails.getNumberOfRepayments(), 5);
});
}
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");