diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java index 008f8c39c6d..132bfb67f8b 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java @@ -29,6 +29,7 @@ import lombok.ToString; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.util.Memo; @ToString(exclude = { "previous" }) @EqualsAndHashCode(exclude = { "previous" }) @@ -49,6 +50,11 @@ public class RepaymentPeriod { @Getter private Money paidInterest; + private Memo rateFactorPlus1Calculation; + private Memo calculatedDueInterestCalculation; + private Memo dueInterestCalculation; + private Memo outstandingBalanceCalculation; + public RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi) { this.previous = previous; this.fromDate = fromDate; @@ -80,10 +86,25 @@ public Optional getPrevious() { } public BigDecimal getRateFactorPlus1() { + if (rateFactorPlus1Calculation == null) { + rateFactorPlus1Calculation = Memo.of(this::calculateRateFactorPlus1, () -> this.interestPeriods); + } + return rateFactorPlus1Calculation.get(); + } + + private BigDecimal calculateRateFactorPlus1() { return interestPeriods.stream().map(InterestPeriod::getRateFactor).reduce(BigDecimal.ONE, BigDecimal::add); } public Money getCalculatedDueInterest() { + if (calculatedDueInterestCalculation == null) { + calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, + () -> new Object[] { this.previous, this.interestPeriods }); + } + return calculatedDueInterestCalculation.get(); + } + + private Money calculateCalculatedDueInterest() { Money calculatedDueInterest = getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(getZero(), Money::plus); if (getPrevious().isPresent()) { @@ -106,9 +127,13 @@ public boolean isFullyPaid() { } public Money getDueInterest() { - // Due interest might be the maximum paid if there is pay-off or early repayment - return MathUtil.max(getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest() : getCalculatedDueInterest(), - getPaidInterest(), false); + if (dueInterestCalculation == null) { + // Due interest might be the maximum paid if there is pay-off or early repayment + dueInterestCalculation = Memo.of(() -> MathUtil.max( + getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest() : getCalculatedDueInterest(), + getPaidInterest(), false), () -> new Object[] { paidPrincipal, paidInterest, interestPeriods }); + } + return dueInterestCalculation.get(); } public Money getDuePrincipal() { @@ -121,13 +146,18 @@ public Money getUnrecognizedInterest() { } public Money getOutstandingLoanBalance() { - InterestPeriod lastInstallmentPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1); - Money calculatedOutStandingLoanBalance = lastInstallmentPeriod.getOutstandingLoanBalance() // - .plus(lastInstallmentPeriod.getBalanceCorrectionAmount()) // - .plus(lastInstallmentPeriod.getDisbursementAmount()) // - .minus(getDuePrincipal())// - .plus(getPaidPrincipal());// - return MathUtil.negativeToZero(calculatedOutStandingLoanBalance); + if (outstandingBalanceCalculation == null) { + outstandingBalanceCalculation = Memo.of(() -> { + InterestPeriod lastInstallmentPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1); + Money calculatedOutStandingLoanBalance = lastInstallmentPeriod.getOutstandingLoanBalance() // + .plus(lastInstallmentPeriod.getBalanceCorrectionAmount()) // + .plus(lastInstallmentPeriod.getDisbursementAmount()) // + .minus(getDuePrincipal())// + .plus(getPaidPrincipal());// + return MathUtil.negativeToZero(calculatedOutStandingLoanBalance); + }, () -> interestPeriods); + } + return outstandingBalanceCalculation.get(); } public void addPaidPrincipalAmount(Money paidPrincipal) { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/util/Memo.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/util/Memo.java new file mode 100644 index 00000000000..dd3f71a12dd --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/util/Memo.java @@ -0,0 +1,103 @@ +/** + * 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.util; + +import java.util.function.Supplier; + +/** + * Memo (Object value cache) for calculations + * + */ +public final class Memo { + + private final Object lock = new Object(); + private final Supplier supplier; + private final Supplier dependenciesGetter; + private final boolean useReferenceCheck; + + private volatile T value; + private volatile int[] dependencyHashCodes = new int[0]; + + private Memo(Supplier supplier, Supplier dependenciesGetter, boolean useReferenceCheck) { + this.supplier = supplier; + this.dependenciesGetter = dependenciesGetter; + this.useReferenceCheck = useReferenceCheck; + } + + public T get() { + Object actualDependencies = dependenciesGetter != null ? dependenciesGetter.get() : null; + if (actualDependencies == null && value != null) { + return value; + } + synchronized (lock) { + if (checkDependencyChangedAndUpdate(actualDependencies)) { + value = supplier.get(); + } + } + return value; + } + + private boolean checkDependencyChangedAndUpdate(Object actualDependencies) { + if (actualDependencies == null) { + return true; + } + if (actualDependencies instanceof Object[] actualDependencyList) { + boolean isSame = dependencyHashCodes.length == actualDependencyList.length; + int[] actualDependencyHashCodes = new int[actualDependencyList.length]; + for (int i = 0; i < actualDependencyList.length; i++) { + actualDependencyHashCodes[i] = getHashCode(actualDependencyList[i]); + if (isSame) { + isSame = dependencyHashCodes[i] == actualDependencyHashCodes[i]; + } + } + if (!isSame) { + dependencyHashCodes = actualDependencyHashCodes; + } + return !isSame; + } else { + final int[] actualDependencyHashCodes = { getHashCode(actualDependencies) }; + final boolean isSame = dependencyHashCodes.length == actualDependencyHashCodes.length + && dependencyHashCodes[0] == actualDependencyHashCodes[0]; + if (!isSame) { + dependencyHashCodes = actualDependencyHashCodes; + return true; + } + } + return false; + } + + private int getHashCode(Object dependency) { + if (dependency == null) { + return 0; + } + return useReferenceCheck ? System.identityHashCode(dependency) : dependency.hashCode(); + } + + public static Memo of(Supplier supplier) { + return new Memo<>(supplier, null, false); + } + + public static Memo of(Supplier supplier, Supplier dependenciesFunction) { + return new Memo<>(supplier, dependenciesFunction, false); + } + + public static Memo of(Supplier supplier, Supplier dependenciesFunction, boolean useReferenceCheck) { + return new Memo<>(supplier, dependenciesFunction, useReferenceCheck); + } +} diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index 6aebb4ad593..6d3de1cd4be 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -46,6 +46,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -180,6 +181,58 @@ public void testEMICalculator_generateInterestScheduleModel() { Assertions.assertEquals(121, interestScheduleModel.getLoanTermInDays()); } + @Test + @Timeout(1) // seconds + public void testEMICalculation_performance() { + + final List 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))); + expectedRepaymentPeriods.add(repayment(7, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 8, 1))); + expectedRepaymentPeriods.add(repayment(8, LocalDate.of(2024, 8, 1), LocalDate.of(2024, 9, 1))); + expectedRepaymentPeriods.add(repayment(9, LocalDate.of(2024, 9, 1), LocalDate.of(2024, 10, 1))); + expectedRepaymentPeriods.add(repayment(10, LocalDate.of(2024, 10, 1), LocalDate.of(2024, 11, 1))); + expectedRepaymentPeriods.add(repayment(11, LocalDate.of(2024, 11, 1), LocalDate.of(2024, 12, 1))); + expectedRepaymentPeriods.add(repayment(12, LocalDate.of(2024, 12, 1), LocalDate.of(2025, 1, 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); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, + loanProductRelatedDetail, installmentAmountInMultiplesOf); + + final Money disbursedAmount = toMoney(100.0); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); + + Assertions.assertEquals(interestSchedule.getLoanTermInDays(), 366); + Assertions.assertEquals(interestSchedule.repaymentPeriods().size(), 12); + + List repaymentPeriods = interestSchedule.repaymentPeriods(); + for (int i = 0; i < repaymentPeriods.size(); i++) { + final RepaymentPeriod repaymentPeriod = repaymentPeriods.get(i); + Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDuePrincipal().getAmount())); + Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDueInterest().getAmount())); + if (i == repaymentPeriods.size() - 1) { + Assertions.assertEquals(0.0, toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount())); + } else { + Assertions.assertEquals(8.65, toDouble(repaymentPeriod.getEmi().getAmount())); + Assertions.assertTrue(0 < toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount())); + } + } + } + @Test public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {