diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index c8a9ccb6f..e972204cd 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -24,6 +24,7 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; +import java.time.Year; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; @@ -34,6 +35,7 @@ import java.util.Locale; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.portfolio.common.domain.DaysInYearType; public final class DateUtils { @@ -393,4 +395,13 @@ public final class DateUtils { } return formatter; } + + public static Integer daysInYear(final DaysInYearType daysInYear, final LocalDate referenceDate) { + return daysInYear.isActual() ? DateUtils.getDaysInYear(referenceDate.getYear()) : daysInYear.getValue(); + } + + public static Integer getDaysInYear(final Integer year) { + return Year.isLeap(year) ? 366 : 365; + } + } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/ratefactor/RateFactorFunctions.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/ratefactor/RateFactorFunctions.java new file mode 100644 index 000000000..c663d4fa7 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/ratefactor/RateFactorFunctions.java @@ -0,0 +1,50 @@ +/** + * 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.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/ratefactor/RateFactorFunctionsTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/ratefactor/RateFactorFunctionsTest.java new file mode 100644 index 000000000..b535992d7 --- /dev/null +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/ratefactor/RateFactorFunctionsTest.java @@ -0,0 +1,107 @@ +/** + * 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.ratefactor; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.util.ArrayList; +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.jetbrains.annotations.NotNull; +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.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class RateFactorFunctionsTest { + + private static MockedStatic moneyHelper = Mockito.mockStatic(MoneyHelper.class); + + private static List periods; + private final BigDecimal interestRate = BigDecimal.valueOf(0.09482); + + @BeforeAll + public static void init() { + periods = new ArrayList<>(); + LocalDate startDate = LocalDate.of(2024, 01, 1); + periods.add(createPeriod(1, startDate, startDate.plusMonths(1))); + periods.add(createPeriod(2, startDate.plusMonths(1), startDate.plusMonths(2))); + periods.add(createPeriod(3, startDate.plusMonths(2), startDate.plusMonths(3))); + periods.add(createPeriod(4, startDate.plusMonths(3), startDate.plusMonths(4))); + periods.add(createPeriod(5, startDate.plusMonths(4), startDate.plusMonths(5))); + periods.add(createPeriod(6, startDate.plusMonths(5), startDate.plusMonths(6))); + + // When + moneyHelper.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.HALF_EVEN); + moneyHelper.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(8, 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" }; + + // Then + for (LoanRepaymentScheduleInstallment period : periods) { + final Long daysInPeriod = DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate()); + final Integer daysInYear = DateUtils.daysInYear(daysInYearType, period.getFromDate()); + BigDecimal rateFactor = RateFactorFunctions.rateFactor(interestRate, daysInPeriod, daysInYear, MoneyHelper.getMathContext()); + + Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], rateFactor.toString()); + } + } + + @Test + 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" }; + + // Then + for (LoanRepaymentScheduleInstallment period : periods) { + final Long daysInPeriod = DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate()); + final Integer daysInYear = DateUtils.daysInYear(daysInYearType, period.getFromDate()); + + BigDecimal rateFactor = RateFactorFunctions.rateFactor(interestRate, daysInPeriod, daysInYear, MoneyHelper.getMathContext()); + + Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], rateFactor.toString()); + } + } + + @NotNull + private static LoanRepaymentScheduleInstallment createPeriod(int periodId, LocalDate start, LocalDate end) { + LoanRepaymentScheduleInstallment period = Mockito.mock(LoanRepaymentScheduleInstallment.class); + Mockito.when(period.getInstallmentNumber()).thenReturn(periodId); + Mockito.when(period.getFromDate()).thenReturn(start); + Mockito.when(period.getDueDate()).thenReturn(end); + + return period; + } + +}