From f0deb111cb6991d39dca548e9ca85821abcef503 Mon Sep 17 00:00:00 2001 From: Janos Meszaros Date: Fri, 7 Jun 2024 15:42:22 +0200 Subject: [PATCH] FINERACT-1981: EMI Calculation --- ...nctions.java => EMICalculationResult.java} | 34 ++-- .../loanproduct/calc/EMICalculator.java | 150 ++++++++++++++++++ .../calc/ratefactor/RateFactorFunctions.java | 50 ------ ...ctionsTest.java => EMICalculatorTest.java} | 55 +++++-- 4 files changed, 204 insertions(+), 85 deletions(-) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/{emi/FnValueFunctions.java => EMICalculationResult.java} (51%) create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java delete mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ratefactor/RateFactorFunctions.java rename fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/{EMICalculationFunctionsTest.java => EMICalculatorTest.java} (66%) diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/emi/FnValueFunctions.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java similarity index 51% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/emi/FnValueFunctions.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java index e7c152afb..0fd12b5ee 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/emi/FnValueFunctions.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java @@ -16,29 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.portfolio.loanproduct.calc.emi; +package org.apache.fineract.portfolio.loanproduct.calc; import java.math.BigDecimal; -import java.math.MathContext; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; -public class FnValueFunctions { +@AllArgsConstructor(access = AccessLevel.PACKAGE) +public class EMICalculationResult { - protected FnValueFunctions() {} + @Getter + private final BigDecimal equalMonthlyInstallmentValue; + private final List repaymentPeriodRateFactorMinus1List; - /** - * To calculate the function value for each period, we are going to use the next formula: - * - * fn = 1 + fnValueFrom * rateFactorEnd - * - * @param previousFnValue - * - * @param currentRateFactor - * - * @param mathContext - * - */ - public static BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal currentRateFactor, final MathContext mc) { - return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, mc)); + public BigDecimal getRateFactorMinus1ForRepaymentPeriod(Integer repaymentPeriodNumber) { + return repaymentPeriodNumber != null && repaymentPeriodNumber > 0 + && repaymentPeriodNumber <= repaymentPeriodRateFactorMinus1List.size() + ? repaymentPeriodRateFactorMinus1List.get(repaymentPeriodNumber - 1) + : BigDecimal.ZERO; } - } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java new file mode 100644 index 000000000..37659e62f --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java @@ -0,0 +1,150 @@ +/** + * 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.math.BigDecimal; +import java.math.MathContext; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class EMICalculator { + + /** + * Calculate EMI parts and return an EMI calculation result object with repayment installment rate factors + * + * @param repaymentPeriodDays + * List of day gaps between periods (zero interest period values should be 0) + * @param principal + * @param interestRate + * @param daysInYear + * @param mc + * @return + */ + public EMICalculationResult calculateEMI(final List repaymentPeriodDays, final BigDecimal principal, + final BigDecimal interestRate, final Integer daysInYear, final MathContext mc) { + final List rateFactorList = getRateFactorList(repaymentPeriodDays, interestRate, daysInYear, mc); + final BigDecimal rateFactorN = calculateRateFactorN(rateFactorList, mc); + final BigDecimal fnResult = calculateFnResult(rateFactorList, mc); + + final BigDecimal emiValue = calculateEMIValue(rateFactorN, principal, fnResult, mc); + final List rateFactorMinus1List = getRateFactorMinus1List(rateFactorList, mc); + + return new EMICalculationResult(emiValue, rateFactorMinus1List); + } + + /** + * Calculate rate factors from repayment periods + * + * @param repaymentPeriodDays + * @param interestRate + * @param daysInYear + * @param mc + * @return + */ + List getRateFactorList(final List repaymentPeriodDays, final BigDecimal interestRate, final Integer daysInYear, + final MathContext mc) { + return repaymentPeriodDays.stream().map(daysInPeriod -> rateFactor(interestRate, daysInPeriod, daysInYear, mc)).toList(); + } + + /** + * Rate factor -1 values + * + * @param rateFactors + * @param mc + * @return + */ + List getRateFactorMinus1List(final List rateFactors, final MathContext mc) { + return rateFactors.stream().map(it -> it.subtract(BigDecimal.ONE, mc)).toList(); + } + + /** + * Calculate Rate Factor Product from rate factors + * + * @param rateFactors + * @param mc + * @return + */ + BigDecimal calculateRateFactorN(final List rateFactors, final MathContext mc) { + return rateFactors.stream().reduce(BigDecimal.ONE, (BigDecimal acc, BigDecimal value) -> acc.multiply(value, mc)); + } + + /** + * Summarize Fn values + * + * @param rateFactors + * @param mc + * @return + */ + BigDecimal calculateFnResult(final List rateFactors, final MathContext mc) { + return rateFactors.stream().skip(1).reduce(BigDecimal.ONE, + (BigDecimal previousValue, BigDecimal rateFactor) -> fnValue(previousValue, rateFactor, mc)); + } + + /** + * Calculate the EMI (Equal Monthly Installment) value + * + * @param rateFactorN + * @param principal + * @param fnResult + * @param mc + * @return + */ + BigDecimal calculateEMIValue(final BigDecimal rateFactorN, final BigDecimal principal, final BigDecimal fnResult, + final MathContext mc) { + return rateFactorN.multiply(principal, mc).divide(fnResult, mc); + } + + /** + * To calculate the monthly payment, we first need to calculate something called the Rate Factor. We're going to be + * using simple interest. The Rate Factor for simple interest is calculated by the following formula: + * + * + * R = 1 + (r * d / y) + * + * @param interestRate + * (r) + * @param daysInPeriod + * (d) + * @param daysInYear + * (y) + */ + BigDecimal rateFactor(final BigDecimal interestRate, final Integer daysInPeriod, final Integer daysInYear, final MathContext mc) { + final BigDecimal daysPeriod = BigDecimal.valueOf(daysInPeriod); + final BigDecimal daysYear = BigDecimal.valueOf(daysInYear); + + return BigDecimal.ONE.add(interestRate.multiply(daysPeriod.divide(daysYear, mc), mc), mc); + } + + /** + * To calculate the function value for each period, we are going to use the next formula: + * + * fn = 1 + fnValueFrom * rateFactorEnd + * + * @param previousFnValue + * + * @param currentRateFactor + * + * @param mc + * + */ + BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal currentRateFactor, final MathContext mc) { + return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, mc), mc); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ratefactor/RateFactorFunctions.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ratefactor/RateFactorFunctions.java deleted file mode 100644 index 99f8b68aa..000000000 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ratefactor/RateFactorFunctions.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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.ratefactor; - -import java.math.BigDecimal; -import java.math.MathContext; - -public class RateFactorFunctions { - - protected RateFactorFunctions() {} - - /** - * To calculate the monthly payment, we first need to calculate something called the Rate Factor. We're going to be - * using simple interest. The Rate Factor for simple interest is calculated by the following formula: - * - * - * R = 1 + (r * d / y) - * - * @param interestRate - * (r) - * @param daysInPeriod - * (d) - * @param daysInYear - * (y) - */ - public static BigDecimal rateFactor(final BigDecimal interestRate, final Long daysInPeriod, final Integer daysInYear, - final MathContext mc) { - final BigDecimal daysPeriod = BigDecimal.valueOf(daysInPeriod); - final BigDecimal daysYear = BigDecimal.valueOf(daysInYear); - - return BigDecimal.ONE.add(interestRate.multiply(daysPeriod.divide(daysYear, mc), mc), mc); - } - -} diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationFunctionsTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculatorTest.java similarity index 66% rename from fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationFunctionsTest.java rename to fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculatorTest.java index e11be4db5..30b819762 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationFunctionsTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculatorTest.java @@ -23,13 +23,12 @@ import java.math.MathContext; import java.math.RoundingMode; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanproduct.calc.emi.FnValueFunctions; -import org.apache.fineract.portfolio.loanproduct.calc.ratefactor.RateFactorFunctions; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -40,12 +39,15 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class EMICalculationFunctionsTest { +class EMICalculatorTest { + + private static EMICalculator emiCalculator = new EMICalculator(); private static MockedStatic moneyHelper = Mockito.mockStatic(MoneyHelper.class); private static List periods; - private final BigDecimal interestRate = BigDecimal.valueOf(0.09482); + private final BigDecimal interestRate = BigDecimal.valueOf(0.094822); + private final BigDecimal principal = BigDecimal.valueOf(100); @BeforeAll public static void init() { @@ -60,20 +62,21 @@ class EMICalculationFunctionsTest { // When moneyHelper.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.HALF_EVEN); - moneyHelper.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(8, RoundingMode.HALF_EVEN)); + moneyHelper.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN)); } @Test public void testRateFactorFunctionDay365() { // Given final DaysInYearType daysInYearType = DaysInYearType.DAYS_365; - final String[] expectedValues = new String[] { "1.0080532", "1.0075336", "1.0080532", "1.0077934", "1.0080532", "1.0077934" }; + final String[] expectedValues = new String[] { "1.00805337534", "1.00753380274", "1.00805337534", "1.00779358904", "1.00805337534", + "1.00779358904" }; // Then for (LoanRepaymentScheduleInstallment period : periods) { - final Long daysInPeriod = DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate()); + final Integer daysInPeriod = Math.toIntExact(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate())); final Integer daysInYear = DateUtils.daysInYear(daysInYearType, period.getFromDate()); - BigDecimal rateFactor = RateFactorFunctions.rateFactor(interestRate, daysInPeriod, daysInYear, MoneyHelper.getMathContext()); + BigDecimal rateFactor = emiCalculator.rateFactor(interestRate, daysInPeriod, daysInYear, MoneyHelper.getMathContext()); Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], rateFactor.toString()); } @@ -83,14 +86,15 @@ class EMICalculationFunctionsTest { public void testRateFactorFunctionActual() { // Given final DaysInYearType daysInYearType = DaysInYearType.ACTUAL; - final String[] expectedValues = new String[] { "1.0080312", "1.0075131", "1.0080312", "1.0077721", "1.0080312", "1.0077721" }; + final String[] expectedValues = new String[] { "1.00803137158", "1.00751321858", "1.00803137158", "1.00777229508", "1.00803137158", + "1.00777229508" }; // Then for (LoanRepaymentScheduleInstallment period : periods) { - final Long daysInPeriod = DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate()); + final Integer daysInPeriod = Math.toIntExact(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate())); final Integer daysInYear = DateUtils.daysInYear(daysInYearType, period.getFromDate()); - BigDecimal rateFactor = RateFactorFunctions.rateFactor(interestRate, daysInPeriod, daysInYear, MoneyHelper.getMathContext()); + BigDecimal rateFactor = emiCalculator.rateFactor(interestRate, daysInPeriod, daysInYear, MoneyHelper.getMathContext()); Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], rateFactor.toString()); } @@ -101,17 +105,17 @@ class EMICalculationFunctionsTest { // Given final DaysInYearType daysInYearType = DaysInYearType.DAYS_365; final MathContext mc = MoneyHelper.getMathContext(); - final String[] expectedValues = new String[] { "1.0000000", "2.0075336", "3.0237007", "4.0472656", "5.0798590", "6.1194484" }; + final String[] expectedValues = new String[] { "1.00000000000", "2.00753380274", "3.02370122596", "4.04726671069", "5.07986086861", + "6.11945121660" }; final List fnValuesCalculated = new ArrayList<>(); BigDecimal previousFnValue = BigDecimal.ZERO; for (LoanRepaymentScheduleInstallment period : periods) { - final Long daysInPeriod = DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate()); + final Integer daysInPeriod = Math.toIntExact(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate())); final Integer daysInYear = DateUtils.daysInYear(daysInYearType, period.getFromDate()); - final BigDecimal rateFactor = RateFactorFunctions.rateFactor(interestRate, daysInPeriod, daysInYear, - MoneyHelper.getMathContext()); + final BigDecimal rateFactor = emiCalculator.rateFactor(interestRate, daysInPeriod, daysInYear, MoneyHelper.getMathContext()); - final BigDecimal currentFnValue = FnValueFunctions.fnValue(previousFnValue, rateFactor, mc); + final BigDecimal currentFnValue = emiCalculator.fnValue(previousFnValue, rateFactor, mc); fnValuesCalculated.add(currentFnValue); previousFnValue = currentFnValue; @@ -123,6 +127,25 @@ class EMICalculationFunctionsTest { } } + @Test + public void testEMICalculation() { + final MathContext mc = MoneyHelper.getMathContext(); + final List repaymentPeriodDays = Arrays.asList(31, 29, 31, 30, 31, 30); + + final EMICalculationResult result = emiCalculator.calculateEMI(repaymentPeriodDays, principal, interestRate, + DaysInYearType.DAYS_365.getValue(), mc); + + // 17.13 + Assertions.assertEquals(BigDecimal.valueOf(17.1293512777), result.getEqualMonthlyInstallmentValue()); + + Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), result.getRateFactorMinus1ForRepaymentPeriod(1)); + Assertions.assertEquals(BigDecimal.valueOf(0.00753380274), result.getRateFactorMinus1ForRepaymentPeriod(2)); + Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), result.getRateFactorMinus1ForRepaymentPeriod(3)); + Assertions.assertEquals(BigDecimal.valueOf(0.00779358904), result.getRateFactorMinus1ForRepaymentPeriod(4)); + Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), result.getRateFactorMinus1ForRepaymentPeriod(5)); + Assertions.assertEquals(BigDecimal.valueOf(0.00779358904), result.getRateFactorMinus1ForRepaymentPeriod(6)); + } + @NotNull private static LoanRepaymentScheduleInstallment createPeriod(int periodId, LocalDate start, LocalDate end) { LoanRepaymentScheduleInstallment period = Mockito.mock(LoanRepaymentScheduleInstallment.class);