This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new db8000b4f FINERACT-1981: EMI Calculation, Daily, Weekly, Monthly
support plug to Repayment Schedule
db8000b4f is described below
commit db8000b4f3ebedfee00a888fe9fb0c099d1828a8
Author: Janos Meszaros <[email protected]>
AuthorDate: Fri Jun 21 17:21:06 2024 +0200
FINERACT-1981: EMI Calculation, Daily, Weekly, Monthly support plug to
Repayment Schedule
---
.../infrastructure/core/service/DateUtils.java | 10 -
.../infrastructure/core/service/MathUtil.java | 10 +
.../portfolio/common/domain/DaysInMonthType.java | 8 +
.../portfolio/common/domain/DaysInYearType.java | 8 +
.../domain/PreGeneratedLoanSchedulePeriod.java | 96 ++++
.../domain/ScheduledDateGenerator.java | 4 +
.../AbstractProgressiveLoanScheduleGenerator.java | 15 +-
.../domain/ProgressiveLoanScheduleGenerator.java | 53 +-
.../loanproduct/calc/EMICalculationResult.java | 17 +-
.../portfolio/loanproduct/calc/EMICalculator.java | 132 +----
.../loanproduct/calc/ProgressiveEMICalculator.java | 437 +++++++++++++++
.../loanproduct/calc/EMICalculatorTest.java | 159 ------
.../calc/ProgressiveEMICalculatorTest.java | 621 +++++++++++++++++++++
.../domain/DefaultScheduledDateGenerator.java | 28 +
.../domain/DefaultScheduledDateGeneratorTest.java | 42 ++
15 files changed, 1312 insertions(+), 328 deletions(-)
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 e972204cd..bd235d4d2 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,7 +24,6 @@ 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;
@@ -35,7 +34,6 @@ 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 {
@@ -396,12 +394,4 @@ 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-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java
index ba58b25f7..7bdd21e46 100644
---
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java
@@ -467,4 +467,14 @@ public final class MathUtil {
}
return percentageOf;
}
+
+ /**
+ * Remove unused zeros from end of BigDecimal fraction and keep the format
"0.0"
+ *
+ * @param value
+ * @return
+ */
+ public static BigDecimal stripTrailingZeros(final BigDecimal value) {
+ return value == null ? null : new
BigDecimal(value.stripTrailingZeros().toPlainString());
+ }
}
diff --git
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInMonthType.java
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInMonthType.java
index 20eca6bfe..c7725d29b 100644
---
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInMonthType.java
+++
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInMonthType.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.common.domain;
+import java.time.LocalDate;
import java.util.Arrays;
/**
@@ -74,4 +75,11 @@ public enum DaysInMonthType {
public boolean isDaysInMonth_30() {
return DaysInMonthType.DAYS_30.getValue().equals(this.value);
}
+
+ public Integer getNumberOfDays(final LocalDate referenceDate) {
+ if (referenceDate == null) {
+ return null;
+ }
+ return this == ACTUAL ? referenceDate.lengthOfMonth() :
this.getValue();
+ }
}
diff --git
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearType.java
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearType.java
index 80a42d484..dde64bf82 100644
---
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearType.java
+++
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearType.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.common.domain;
+import java.time.LocalDate;
import java.util.Arrays;
/**
@@ -83,4 +84,11 @@ public enum DaysInYearType {
public boolean isActual() {
return DaysInYearType.ACTUAL.getValue().equals(this.value);
}
+
+ public Integer getNumberOfDays(final LocalDate referenceDate) {
+ if (referenceDate == null) {
+ return null;
+ }
+ return this == ACTUAL ? referenceDate.lengthOfYear() : this.getValue();
+ }
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PreGeneratedLoanSchedulePeriod.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PreGeneratedLoanSchedulePeriod.java
new file mode 100644
index 000000000..636cf0046
--- /dev/null
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PreGeneratedLoanSchedulePeriod.java
@@ -0,0 +1,96 @@
+/**
+ * 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.loanaccount.loanschedule.domain;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Set;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
+import
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
+
+@Data
+@Accessors(fluent = true)
+public class PreGeneratedLoanSchedulePeriod implements LoanScheduleModelPeriod
{
+
+ private Integer periodNumber;
+ private boolean isRepaymentPeriod;
+ private boolean isDownPaymentPeriod;
+ private LocalDate periodFromDate;
+ private LocalDate periodDueDate;
+ private BigDecimal principalDue;
+ private BigDecimal interestDue;
+ private BigDecimal feeChargesDue;
+ private BigDecimal penaltyChargesDue;
+ private BigDecimal rescheduleInterestPortion;
+ private boolean isRecalculatedInterestComponent;
+ private boolean isEMIFixedSpecificToInstallment;
+
+ public PreGeneratedLoanSchedulePeriod(Integer periodNumber, LocalDate
periodFromDate, LocalDate periodDueDate) {
+ this.periodNumber = periodNumber;
+ this.isRepaymentPeriod = true;
+ this.isDownPaymentPeriod = false;
+ this.periodFromDate = periodFromDate;
+ this.periodDueDate = periodDueDate;
+ this.principalDue = BigDecimal.ZERO;
+ this.interestDue = BigDecimal.ZERO;
+ this.feeChargesDue = BigDecimal.ZERO;
+ this.penaltyChargesDue = BigDecimal.ZERO;
+ this.rescheduleInterestPortion = BigDecimal.ZERO;
+ this.isRecalculatedInterestComponent = false;
+ this.isEMIFixedSpecificToInstallment = false;
+ }
+
+ @Override
+ public LoanSchedulePeriodData toData() {
+ return null;
+ }
+
+ @Override
+ public void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge)
{
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addPrincipalAmount(Money principalDue) {
+ this.principalDue = this.principalDue.add(principalDue.getAmount());
+ }
+
+ @Override
+ public void addInterestAmount(Money interestDue) {
+ this.interestDue = this.principalDue.add(interestDue.getAmount());
+ }
+
+ @Override
+ public Set<LoanInterestRecalcualtionAdditionalDetails>
getLoanCompoundingDetails() {
+ return Set.of();
+ }
+
+ @Override
+ public void setEMIFixedSpecificToInstallmentTrue() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setRescheduleInterestPortion(BigDecimal
rescheduleInterestPortion) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
index 50edf46fa..1fd14d85f 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import java.time.LocalDate;
+import java.util.List;
import
org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
@@ -26,6 +27,9 @@ import
org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
public interface ScheduledDateGenerator {
+ List<PreGeneratedLoanSchedulePeriod>
generateRepaymentPeriods(LoanApplicationTerms loanApplicationTerms,
+ HolidayDetailDTO holidayDetailDTO);
+
LocalDate getLastRepaymentDate(LoanApplicationTerms loanApplicationTerms,
HolidayDetailDTO holidayDetailDTO);
LocalDate
idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType
repaymentPeriodFrequencyType, int repaidEvery,
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
index 4984ed514..960e79dd7 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
@@ -48,6 +48,8 @@ import
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleD
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.exception.MultiDisbursementOutstandingAmoutException;
+import org.apache.fineract.portfolio.loanproduct.calc.EMICalculationResult;
+import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
public abstract class AbstractProgressiveLoanScheduleGenerator implements
LoanScheduleGenerator {
@@ -83,6 +85,11 @@ public abstract class
AbstractProgressiveLoanScheduleGenerator implements LoanSc
List<LoanScheduleModelPeriod> periods =
createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms,
scheduleParams,
chargesDueAtTimeOfDisbursement);
+ List<PreGeneratedLoanSchedulePeriod> expectedRepaymentPeriods =
getScheduledDateGenerator()
+ .generateRepaymentPeriods(loanApplicationTerms,
holidayDetailDTO);
+ EMICalculationResult emiCalculationResult =
getEMICalculator().calculateEMIValueAndRateFactors(scheduleParams,
+ loanApplicationTerms.toLoanProductRelatedDetail(),
expectedRepaymentPeriods, mc);
+
boolean isFirstRepayment = true;
// charges which depends on total loan interest will be added to this
@@ -115,10 +122,11 @@ public abstract class
AbstractProgressiveLoanScheduleGenerator implements LoanSc
// 5 determine principal,interest of repayment period
PrincipalInterest principalInterestForThisPeriod =
calculatePrincipalInterestComponentsForPeriod(loanApplicationTerms,
- scheduleParams, mc);
+ scheduleParams, emiCalculationResult, mc);
// update cumulative fields for principal
currentPeriodParams.setPrincipalForThisPeriod(principalInterestForThisPeriod.principal());
+
currentPeriodParams.setInterestForThisPeriod(principalInterestForThisPeriod.interest());
updateOutstandingBalance(scheduleParams, currentPeriodParams);
if (scheduleParams.getOutstandingBalance().isLessThanZero() ||
!isNextRepaymentAvailable) {
@@ -226,7 +234,9 @@ public abstract class
AbstractProgressiveLoanScheduleGenerator implements LoanSc
public abstract PaymentPeriodsInOneYearCalculator
getPaymentPeriodsInOneYearCalculator();
public abstract PrincipalInterest
calculatePrincipalInterestComponentsForPeriod(LoanApplicationTerms
loanApplicationTerms,
- LoanScheduleParams loanScheduleParams, MathContext mc);
+ LoanScheduleParams loanScheduleParams, EMICalculationResult
emiCalculationResult, MathContext mc);
+
+ protected abstract EMICalculator getEMICalculator();
// Private, internal methods
private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final
Set<LoanCharge> loanCharges) {
@@ -260,7 +270,6 @@ public abstract class
AbstractProgressiveLoanScheduleGenerator implements LoanSc
loanScheduleParams.addOutstandingBalance(principalMoney);
loanScheduleParams.addOutstandingBalanceAsPerRest(principalMoney);
loanScheduleParams.addPrincipalToBeScheduled(principalMoney);
-
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(principalMoney));
loanApplicationTerms.resetFixedEmiAmount();
if (loanApplicationTerms.isDownPaymentEnabled()) {
final LoanScheduleModelDownPaymentPeriod downPaymentPeriod
= createDownPaymentPeriod(loanApplicationTerms,
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
index f05675e3c..10bb88987 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
@@ -23,6 +23,8 @@ import java.math.MathContext;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.organisation.monetary.domain.Money;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
+import org.apache.fineract.portfolio.loanproduct.calc.EMICalculationResult;
+import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.springframework.stereotype.Component;
@Component
@@ -31,6 +33,7 @@ public class ProgressiveLoanScheduleGenerator extends
AbstractProgressiveLoanSch
private final ScheduledDateGenerator scheduledDateGenerator;
private final PaymentPeriodsInOneYearCalculator
paymentPeriodsInOneYearCalculator;
+ private final EMICalculator emiCalculator;
@Override
public ScheduledDateGenerator getScheduledDateGenerator() {
@@ -43,30 +46,32 @@ public class ProgressiveLoanScheduleGenerator extends
AbstractProgressiveLoanSch
}
@Override
- public PrincipalInterest
calculatePrincipalInterestComponentsForPeriod(LoanApplicationTerms
loanApplicationTerms,
- LoanScheduleParams loanScheduleParams, MathContext mc) {
- // TODO: handle interest calculation
- int periodNumber = loanScheduleParams.getPeriodNumber();
- BigDecimal fixedEMIAmount = loanApplicationTerms.getFixedEmiAmount();
- Money calculatedPrincipal;
- if (fixedEMIAmount == null) {
- int noRemainingPeriod =
loanApplicationTerms.getActualNoOfRepaymnets() - periodNumber + 1;
- calculatedPrincipal =
loanScheduleParams.getOutstandingBalance().dividedBy(noRemainingPeriod,
mc.getRoundingMode());
- if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() !=
null) {
- calculatedPrincipal =
Money.roundToMultiplesOf(calculatedPrincipal,
-
loanApplicationTerms.getInstallmentAmountInMultiplesOf());
- }
-
loanApplicationTerms.setFixedEmiAmount(calculatedPrincipal.getAmount());
- } else {
- calculatedPrincipal = Money.of(loanApplicationTerms.getCurrency(),
fixedEMIAmount);
- }
- // adjust if needed
- if (periodNumber == loanApplicationTerms.getActualNoOfRepaymnets()) {
- Money remainingAmount =
loanScheduleParams.getOutstandingBalance().minus(calculatedPrincipal);
- calculatedPrincipal = calculatedPrincipal.plus(remainingAmount);
- }
+ protected EMICalculator getEMICalculator() {
+ return emiCalculator;
+ }
+
+ @Override
+ public PrincipalInterest
calculatePrincipalInterestComponentsForPeriod(final LoanApplicationTerms
loanApplicationTerms,
+ final LoanScheduleParams loanScheduleParams, final
EMICalculationResult emiCalculationResult, final MathContext mc) {
- return new PrincipalInterest(calculatedPrincipal,
Money.zero(loanApplicationTerms.getCurrency()),
- Money.zero(loanApplicationTerms.getCurrency()));
+ final Money equalMonthlyInstallmentValue =
Money.of(loanApplicationTerms.getCurrency(),
+ emiCalculationResult.getEqualMonthlyInstallmentValue());
+ final BigDecimal rateFactorMinus1 =
emiCalculationResult.getNextRepaymentPeriodRateFactorMinus1();
+ final Money calculatedInterest =
loanScheduleParams.getOutstandingBalance().multipliedBy(rateFactorMinus1);
+ final Money calculatedPrincipal =
equalMonthlyInstallmentValue.minus(calculatedInterest);
+
+ return new PrincipalInterest(
+
adjustCalculatedPrincipalWithRemainingBalanceInLastPeriod(calculatedPrincipal,
loanApplicationTerms, loanScheduleParams),
+ calculatedInterest,
Money.zero(loanApplicationTerms.getCurrency()));
+ }
+
+ private Money
adjustCalculatedPrincipalWithRemainingBalanceInLastPeriod(final Money
calculatedPrincipal,
+ final LoanApplicationTerms loanApplicationTerms, final
LoanScheduleParams loanScheduleParams) {
+ final boolean isLastRepaymentPeriod =
loanScheduleParams.getPeriodNumber() ==
loanApplicationTerms.getActualNoOfRepaymnets();
+ if (isLastRepaymentPeriod) {
+ final Money remainingAmount =
loanScheduleParams.getOutstandingBalance().minus(calculatedPrincipal);
+ return calculatedPrincipal.plus(remainingAmount);
+ }
+ return calculatedPrincipal;
}
}
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java
index 0fd12b5ee..71b9435c1 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java
@@ -21,20 +21,23 @@ package org.apache.fineract.portfolio.loanproduct.calc;
import java.math.BigDecimal;
import java.util.List;
import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
import lombok.Getter;
+import lombok.RequiredArgsConstructor;
-@AllArgsConstructor(access = AccessLevel.PACKAGE)
+@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class EMICalculationResult {
@Getter
private final BigDecimal equalMonthlyInstallmentValue;
private final List<BigDecimal> repaymentPeriodRateFactorMinus1List;
- public BigDecimal getRateFactorMinus1ForRepaymentPeriod(Integer
repaymentPeriodNumber) {
- return repaymentPeriodNumber != null && repaymentPeriodNumber > 0
- && repaymentPeriodNumber <=
repaymentPeriodRateFactorMinus1List.size()
- ?
repaymentPeriodRateFactorMinus1List.get(repaymentPeriodNumber - 1)
- : BigDecimal.ZERO;
+ private int counter = 0;
+
+ public BigDecimal getNextRepaymentPeriodRateFactorMinus1() {
+ return counter < repaymentPeriodRateFactorMinus1List.size() ?
repaymentPeriodRateFactorMinus1List.get(counter++) : BigDecimal.ZERO;
+ }
+
+ public void reset() {
+ counter = 0;
}
}
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
index 37659e62f..bf217b73e 100644
---
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
@@ -18,133 +18,15 @@
*/
package org.apache.fineract.portfolio.loanproduct.calc;
-import java.math.BigDecimal;
import java.math.MathContext;
import java.util.List;
-import org.springframework.stereotype.Component;
+import
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
+import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
+import
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
-@Component
-public class EMICalculator {
+public interface 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<Integer>
repaymentPeriodDays, final BigDecimal principal,
- final BigDecimal interestRate, final Integer daysInYear, final
MathContext mc) {
- final List<BigDecimal> 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<BigDecimal> 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<BigDecimal> getRateFactorList(final List<Integer>
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<BigDecimal> getRateFactorMinus1List(final List<BigDecimal>
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<BigDecimal> 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<BigDecimal> 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);
- }
+ EMICalculationResult calculateEMIValueAndRateFactors(LoanScheduleParams
scheduleParams,
+ LoanProductRelatedDetail loanProductRelatedDetail, List<? extends
LoanScheduleModelPeriod> expectedRepaymentPeriods,
+ MathContext mc);
}
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
new file mode 100644
index 000000000..477ff9377
--- /dev/null
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
@@ -0,0 +1,437 @@
+/**
+ * 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.time.LocalDate;
+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.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.LoanScheduleModelPeriod;
+import
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
+import org.springframework.stereotype.Component;
+
+@Component
+public final class ProgressiveEMICalculator implements EMICalculator {
+
+ private static final BigDecimal DIVISOR_100 = new BigDecimal("100");
+ private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7);
+
+ /**
+ * Calculate Equal Monthly Installment value and Rate Factor -1 values for
calculate Interest
+ *
+ * @param scheduleParams
+ * Loan Schedule Params
+ *
+ * @param loanProductRelatedDetail
+ * Loan Product Related Detail from LoanTermApplication
+ *
+ * @param expectedRepaymentPeriods
+ * Expected Repayment Periods
+ *
+ * @param mc
+ * @return
+ */
+ @Override
+ public EMICalculationResult calculateEMIValueAndRateFactors(final
LoanScheduleParams scheduleParams,
+ final LoanProductRelatedDetail loanProductRelatedDetail, final
List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods,
+ final MathContext mc) {
+ // TODO: decide which outstanding balance method needed in the future
+ final BigDecimal nominalInterestRatePerPeriod =
calcNominalInterestRatePerPeriod(
+ loanProductRelatedDetail.getNominalInterestRatePerPeriod(),
mc);
+ final BigDecimal outstandingBalance =
scheduleParams.getOutstandingBalanceAsPerRest().getAmount().compareTo(BigDecimal.ZERO)
> 0
+ ? scheduleParams.getOutstandingBalanceAsPerRest().getAmount()
+ : loanProductRelatedDetail.getPrincipal().getAmount();
+ final DaysInYearType daysInYearType =
DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
+ final DaysInMonthType daysInMonthType =
DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType());
+ final PeriodFrequencyType repaymentFrequency =
loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
+ final BigDecimal repaymentEvery =
BigDecimal.valueOf(loanProductRelatedDetail.getRepayEvery());
+
+ final List<BigDecimal> rateFactorList =
getRateFactorList(expectedRepaymentPeriods, nominalInterestRatePerPeriod,
daysInYearType,
+ daysInMonthType, repaymentFrequency, repaymentEvery, mc);
+
+ return calculateEMI(rateFactorList, outstandingBalance, mc);
+ }
+
+ /**
+ * Convert Interest Percentage to fraction of 1
+ *
+ * @param interestRate
+ * Interest Rate in Percentage
+ *
+ * @param mc
+ *
+ * @return Rate Interest Rate in fraction format
+ */
+ BigDecimal calcNominalInterestRatePerPeriod(final BigDecimal interestRate,
MathContext mc) {
+ return MathUtil.nullToZero(interestRate).divide(DIVISOR_100, mc);
+ }
+
+ /**
+ * * Calculate rate factors from ONLY repayment periods
+ *
+ * @param expectedRepaymentPeriods
+ * @param interestRate
+ * @param daysInYearType
+ * @param daysInMonthType
+ * @param repaymentFrequency
+ * @param repaymentEvery
+ * @param mc
+ * @return
+ */
+ List<BigDecimal> getRateFactorList(final List<? extends
LoanScheduleModelPeriod> expectedRepaymentPeriods,
+ final BigDecimal interestRate, final DaysInYearType
daysInYearType, final DaysInMonthType daysInMonthType,
+ final PeriodFrequencyType repaymentFrequency, final BigDecimal
repaymentEvery, final MathContext mc) {
+ return
expectedRepaymentPeriods.stream().filter(LoanScheduleModelPeriod::isRepaymentPeriod)
+ .map(period ->
MathUtil.stripTrailingZeros(calculateRateFactorPerPeriod(period, interestRate,
daysInYearType,
+ daysInMonthType, repaymentFrequency, repaymentEvery,
mc)))
+ .toList();
+ }
+
+ /**
+ * Calculate Rate Factor for an exact Period
+ *
+ * @param repaymentPeriod
+ * @param interestRate
+ * @param daysInYearType
+ * @param daysInMonthType
+ * @param repaymentFrequency
+ * @param repaymentEvery
+ * @param mc
+ * @return
+ */
+ BigDecimal calculateRateFactorPerPeriod(final LoanScheduleModelPeriod
repaymentPeriod, final BigDecimal interestRate,
+ final DaysInYearType daysInYearType, final DaysInMonthType
daysInMonthType, final PeriodFrequencyType repaymentFrequency,
+ final BigDecimal repaymentEvery, final MathContext mc) {
+ BigDecimal daysInMonth =
BigDecimal.valueOf(daysInMonthType.getNumberOfDays(repaymentPeriod.periodFromDate()));
+ BigDecimal daysInYear =
BigDecimal.valueOf(daysInYearType.getNumberOfDays(repaymentPeriod.periodFromDate()));
+ final BigDecimal actualDaysInPeriod = BigDecimal
+
.valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.periodFromDate(),
repaymentPeriod.periodDueDate()));
+ final BigDecimal calculatedDaysInPeriod = BigDecimal
+
.valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.periodFromDate(),
repaymentPeriod.periodDueDate()));
+ final int numberOfYearsDifferenceInPeriod =
repaymentPeriod.periodDueDate().getYear() -
repaymentPeriod.periodFromDate().getYear();
+ final boolean partialPeriodCalculationNeeded = daysInYearType ==
DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0;
+
+ // TODO check:
loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); //
calculate period data
+ // TODO review: (repayment frequency: days, weeks, years; validation
day is month fix 30)
+ if (partialPeriodCalculationNeeded) {
+ final BigDecimal cumulatedPeriodFractions =
calculatePeriodFractions(repaymentPeriod, mc);
+ return rateFactorByRepaymentPartialPeriod(interestRate,
repaymentEvery, cumulatedPeriodFractions, BigDecimal.ONE,
+ BigDecimal.ONE, mc);
+ }
+
+ return
calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate,
repaymentFrequency, repaymentEvery, daysInMonth,
+ daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
+ }
+
+ /**
+ * Calculate Period fractions part based on how much year has in the period
+ *
+ * @param repaymentPeriod
+ * @param mc
+ * @return
+ */
+ BigDecimal calculatePeriodFractions(LoanScheduleModelPeriod
repaymentPeriod, MathContext mc) {
+ BigDecimal cumulatedRateFactor = BigDecimal.ZERO;
+ int actualYear = repaymentPeriod.periodFromDate().getYear();
+ int endYear = repaymentPeriod.periodDueDate().getYear();
+ LocalDate actualDate = repaymentPeriod.periodFromDate();
+ LocalDate endOfActualYear;
+
+ while (actualYear <= endYear) {
+ endOfActualYear = actualYear == endYear ?
repaymentPeriod.periodDueDate() : LocalDate.of(actualYear, 12, 31);
+ BigDecimal numberOfDaysInYear =
BigDecimal.valueOf(Year.of(actualYear).length());
+ BigDecimal calculatedDaysInActualYear =
BigDecimal.valueOf(DateUtils.getDifferenceInDays(actualDate, endOfActualYear));
+ cumulatedRateFactor =
cumulatedRateFactor.add(calculatedDaysInActualYear.divide(numberOfDaysInYear,
mc), mc);
+ actualDate = endOfActualYear;
+ actualYear++;
+ }
+ return cumulatedRateFactor;
+ }
+
+ /**
+ * Calculate Rate Factor based on Repayment Frequency Type
+ *
+ * @param interestRate
+ * @param repaymentFrequency
+ * @param repaymentEvery
+ * @param daysInMonth
+ * @param daysInYear
+ * @param actualDaysInPeriod
+ * @param calculatedDaysInPeriod
+ * @param mc
+ * @return
+ */
+ BigDecimal calculateRateFactorPerPeriodBasedOnRepaymentFrequency(final
BigDecimal interestRate,
+ final PeriodFrequencyType repaymentFrequency, final BigDecimal
repaymentEvery, final BigDecimal daysInMonth,
+ final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod,
final BigDecimal calculatedDaysInPeriod,
+ final MathContext mc) {
+ return switch (repaymentFrequency) {
+ case DAYS ->
+ rateFactorByRepaymentEveryDay(interestRate, repaymentEvery,
daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
+ case WEEKS ->
+ rateFactorByRepaymentEveryWeek(interestRate, repaymentEvery,
daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
+ case MONTHS -> rateFactorByRepaymentEveryMonth(interestRate,
repaymentEvery, daysInMonth, daysInYear, actualDaysInPeriod,
+ calculatedDaysInPeriod, mc);
+ default -> throw new UnsupportedOperationException("Invalid
repayment frequency"); // not supported yet
+ };
+ }
+
+ /**
+ * Calculate EMI parts and return an EMI calculation result object with
repayment installment rate factors
+ *
+ * @param rateFactorList
+ * @param principal
+ * @param mc
+ * @return
+ */
+ EMICalculationResult calculateEMI(final List<BigDecimal> rateFactorList,
final BigDecimal principal, 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 List<BigDecimal> rateFactorMinus1List =
getRateFactorMinus1List(rateFactorList, mc);
+
+ return new EMICalculationResult(emiValue, rateFactorMinus1List);
+ }
+
+ /**
+ * Rate factor -1 values
+ *
+ * @param rateFactors
+ * @param mc
+ * @return
+ */
+ List<BigDecimal> getRateFactorMinus1List(final List<BigDecimal>
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<BigDecimal> 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<BigDecimal> 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 daily 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:
+ *
+ * Rate factor = 1 + (rate of interest * (repaid every / days in year) *
actual days in period / calculated days in
+ * period ) Where
+ *
+ * @param interestRate
+ * Rate of Interest
+ *
+ * @param repaymentEvery
+ * Repaid Every
+ *
+ * @param daysInYear
+ * Days is Year based on DaysInYear enum
+ *
+ * @param actualDaysInPeriod
+ * Always the actual number of days in the actual period
+ *
+ * @param calculatedDaysInPeriod
+ * Calculated days in Period (It has importance related to
Reschedule)
+ *
+ * @param mc
+ * @return Rate Factor for period
+ */
+ BigDecimal rateFactorByRepaymentEveryDay(final BigDecimal interestRate,
final BigDecimal repaymentEvery, final BigDecimal daysInYear,
+ final BigDecimal actualDaysInPeriod, final BigDecimal
calculatedDaysInPeriod, final MathContext mc) {
+ return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE,
repaymentEvery, daysInYear, actualDaysInPeriod,
+ calculatedDaysInPeriod, mc);
+ }
+
+ /**
+ * To calculate the weekly 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:
+ *
+ * Rate factor = 1 + (rate of interest * (7 * repaid every / days in year)
* actual days in period / calculated days
+ * in period ) Where
+ *
+ * @param interestRate
+ * Rate of Interest
+ *
+ * @param repaymentEvery
+ * Repaid Every
+ *
+ * @param daysInYear
+ * Days is Year based on DaysInYear enum
+ *
+ * @param actualDaysInPeriod
+ * Always the actual number of days in the actual period
+ *
+ * @param calculatedDaysInPeriod
+ * Calculated days in Period (It has importance related to
Reschedule)
+ *
+ * @param mc
+ * @return Rate Factor for period
+ */
+ BigDecimal rateFactorByRepaymentEveryWeek(final BigDecimal interestRate,
final BigDecimal repaymentEvery, final BigDecimal daysInYear,
+ final BigDecimal actualDaysInPeriod, final BigDecimal
calculatedDaysInPeriod, final MathContext mc) {
+ return rateFactorByRepaymentPeriod(interestRate, ONE_WEEK_IN_DAYS,
repaymentEvery, daysInYear, actualDaysInPeriod,
+ calculatedDaysInPeriod, 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:
+ *
+ * Rate factor = 1 + (rate of interest * (days in month * repaid every /
days in year) * actual days in period /
+ * calculated days in period ) Where
+ *
+ * @param interestRate
+ * Rate of Interest
+ *
+ * @param repaymentEvery
+ * Repaid Every
+ *
+ * @param daysInMonth
+ * Days in Month based on DaysInMonth enum
+ *
+ * @param daysInYear
+ * Days is Year based on DaysInYear enum
+ *
+ * @param actualDaysInPeriod
+ * Always the actual number of days in the actual period
+ *
+ * @param calculatedDaysInPeriod
+ * Calculated days in Period (It has importance related to
Reschedule)
+ *
+ * @param mc
+ * @return Rate Factor for period
+ */
+ BigDecimal rateFactorByRepaymentEveryMonth(final BigDecimal interestRate,
final BigDecimal repaymentEvery, final BigDecimal daysInMonth,
+ final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod,
final BigDecimal calculatedDaysInPeriod,
+ final MathContext mc) {
+ return rateFactorByRepaymentPeriod(interestRate, daysInMonth,
repaymentEvery, daysInYear, actualDaysInPeriod,
+ calculatedDaysInPeriod, mc);
+ }
+
+ /**
+ * To calculate installment period payment. We're going to be using simple
interest. The Rate Factor for simple
+ * interest is calculated by the following formula:
+ *
+ * Rate factor = 1 + (rate of interest * ( repayment period multiplier in
days * repaid every * days in month / days
+ * in year) * actual days in period / calculated days in period ) Where
+ *
+ * @param interestRate
+ * Rate of Interest
+ *
+ * @param repaymentPeriodMultiplierInDays
+ * Multiplier number in days of the repayment every parameter
+ *
+ * @param repaymentEvery
+ * Repaid Every
+ *
+ * @param daysInYear
+ * Days is Year based on DaysInYear enum
+ *
+ * @param actualDaysInPeriod
+ * Always the actual number of days in the actual period
+ *
+ * @param calculatedDaysInPeriod
+ * Calculated days in Period (It has importance related to
Reschedule)
+ *
+ * @param mc
+ * @return Rate Factor for period
+ */
+ BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate,
final BigDecimal repaymentPeriodMultiplierInDays,
+ final BigDecimal repaymentEvery, final BigDecimal daysInYear,
final BigDecimal actualDaysInPeriod,
+ final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
+ final BigDecimal interestFractionPerPeriod =
repaymentPeriodMultiplierInDays.multiply(repaymentEvery, mc).divide(daysInYear,
mc);
+ return BigDecimal.ONE.add(
+ interestRate.multiply(interestFractionPerPeriod,
mc).multiply(actualDaysInPeriod, mc).divide(calculatedDaysInPeriod, mc),
+ mc);
+ }
+
+ /**
+ * Calculate Rate Factor based on Partial Period
+ *
+ * @param interestRate
+ * @param repaymentEvery
+ * @param cumulatedPeriodRatio
+ * @param actualDaysInPeriod
+ * @param calculatedDaysInPeriod
+ * @param mc
+ * @return
+ */
+ BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal
interestRate, final BigDecimal repaymentEvery,
+ final BigDecimal cumulatedPeriodRatio, final BigDecimal
actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
+ final MathContext mc) {
+ final BigDecimal interestFractionPerPeriod =
repaymentEvery.multiply(cumulatedPeriodRatio);
+ return BigDecimal.ONE.add(
+ interestRate.multiply(interestFractionPerPeriod,
mc).multiply(actualDaysInPeriod, mc).divide(calculatedDaysInPeriod, 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/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculatorTest.java
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculatorTest.java
deleted file mode 100644
index 30b819762..000000000
---
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculatorTest.java
+++ /dev/null
@@ -1,159 +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;
-
-import java.math.BigDecimal;
-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.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 EMICalculatorTest {
-
- private static EMICalculator emiCalculator = new EMICalculator();
-
- private static MockedStatic<MoneyHelper> moneyHelper =
Mockito.mockStatic(MoneyHelper.class);
-
- private static List<LoanRepaymentScheduleInstallment> periods;
- private final BigDecimal interestRate = BigDecimal.valueOf(0.094822);
- private final BigDecimal principal = BigDecimal.valueOf(100);
-
- @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(12, RoundingMode.HALF_EVEN));
- }
-
- @Test
- public void testRateFactorFunctionDay365() {
- // Given
- final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
- final String[] expectedValues = new String[] { "1.00805337534",
"1.00753380274", "1.00805337534", "1.00779358904", "1.00805337534",
- "1.00779358904" };
-
- // Then
- for (LoanRepaymentScheduleInstallment period : periods) {
- final Integer daysInPeriod =
Math.toIntExact(DateUtils.getDifferenceInDays(period.getFromDate(),
period.getDueDate()));
- final Integer daysInYear = DateUtils.daysInYear(daysInYearType,
period.getFromDate());
- BigDecimal rateFactor = emiCalculator.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.00803137158",
"1.00751321858", "1.00803137158", "1.00777229508", "1.00803137158",
- "1.00777229508" };
-
- // Then
- for (LoanRepaymentScheduleInstallment period : periods) {
- final Integer daysInPeriod =
Math.toIntExact(DateUtils.getDifferenceInDays(period.getFromDate(),
period.getDueDate()));
- final Integer daysInYear = DateUtils.daysInYear(daysInYearType,
period.getFromDate());
-
- BigDecimal rateFactor = emiCalculator.rateFactor(interestRate,
daysInPeriod, daysInYear, MoneyHelper.getMathContext());
-
-
Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1],
rateFactor.toString());
- }
- }
-
- @Test
- public void testFnValueFunctionDay365() {
- // Given
- final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
- final MathContext mc = MoneyHelper.getMathContext();
- final String[] expectedValues = new String[] { "1.00000000000",
"2.00753380274", "3.02370122596", "4.04726671069", "5.07986086861",
- "6.11945121660" };
-
- final List<BigDecimal> fnValuesCalculated = new ArrayList<>();
- BigDecimal previousFnValue = BigDecimal.ZERO;
- for (LoanRepaymentScheduleInstallment period : periods) {
- final Integer daysInPeriod =
Math.toIntExact(DateUtils.getDifferenceInDays(period.getFromDate(),
period.getDueDate()));
- final Integer daysInYear = DateUtils.daysInYear(daysInYearType,
period.getFromDate());
- final BigDecimal rateFactor =
emiCalculator.rateFactor(interestRate, daysInPeriod, daysInYear,
MoneyHelper.getMathContext());
-
- final BigDecimal currentFnValue =
emiCalculator.fnValue(previousFnValue, rateFactor, mc);
- fnValuesCalculated.add(currentFnValue);
-
- previousFnValue = currentFnValue;
- }
-
- int idx = 0;
- for (BigDecimal fnValue : fnValuesCalculated) {
- Assertions.assertEquals(expectedValues[idx++], fnValue.toString());
- }
- }
-
- @Test
- public void testEMICalculation() {
- final MathContext mc = MoneyHelper.getMathContext();
- final List<Integer> 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);
- Mockito.when(period.getInstallmentNumber()).thenReturn(periodId);
- Mockito.when(period.getFromDate()).thenReturn(start);
- Mockito.when(period.getDueDate()).thenReturn(end);
-
- return period;
- }
-
-}
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
new file mode 100644
index 000000000..447948e8b
--- /dev/null
+++
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
@@ -0,0 +1,621 @@
+/**
+ * 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.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.ApplicationCurrency;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
+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.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.LoanScheduleModelDisbursementPeriod;
+import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
+import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PreGeneratedLoanSchedulePeriod;
+import
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
+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 ProgressiveEMICalculatorTest {
+
+ private static final ProgressiveEMICalculator emiCalculator = new
ProgressiveEMICalculator();
+
+ private static MockedStatic<MoneyHelper> moneyHelper =
Mockito.mockStatic(MoneyHelper.class);
+ private static LoanScheduleParams scheduleParams =
Mockito.mock(LoanScheduleParams.class);
+ private static LoanProductRelatedDetail loanProductRelatedDetail =
Mockito.mock(LoanProductRelatedDetail.class);
+
+ private static final MonetaryCurrency monetaryCurrency = MonetaryCurrency
+ .fromApplicationCurrency(new ApplicationCurrency("USD", "USD", 2,
1, "USD", "$"));
+
+ private static List<LoanRepaymentScheduleInstallment> periods;
+ private final BigDecimal interestRate = BigDecimal.valueOf(0.094822);
+
+ @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(12, RoundingMode.HALF_EVEN));
+ }
+
+ private BigDecimal getRateFactorsByMonth(final DaysInYearType
daysInYearType, final DaysInMonthType daysInMonthType,
+ final BigDecimal interestRate, LoanRepaymentScheduleInstallment
period) {
+ final BigDecimal daysInPeriod =
BigDecimal.valueOf(DateUtils.getDifferenceInDays(period.getFromDate(),
period.getDueDate()));
+ final BigDecimal daysInYear =
BigDecimal.valueOf(daysInYearType.getNumberOfDays(period.getFromDate()));
+ final BigDecimal daysInMonth =
BigDecimal.valueOf(daysInMonthType.getNumberOfDays(period.getFromDate()));
+ return emiCalculator.rateFactorByRepaymentEveryMonth(interestRate,
BigDecimal.ONE, daysInMonth, daysInYear, daysInPeriod,
+ daysInPeriod, MoneyHelper.getMathContext());
+ }
+
+ @Test
+ public void
testRateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthActual() {
+ // Given
+ final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
+ final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
+ final String[] expectedValues = new String[] { "1.00805337534",
"1.00753380274", "1.00805337534", "1.00779358904", "1.00805337534",
+ "1.00779358904" };
+
+ // Then
+ for (LoanRepaymentScheduleInstallment period : periods) {
+ BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType,
daysInMonthType, interestRate, period);
+
Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1],
rateFactor.toString());
+ }
+ }
+
+ @Test
+ public void
testRateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMonthActual() {
+ // Given
+ final DaysInYearType daysInYearType = DaysInYearType.ACTUAL;
+ final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
+ final String[] expectedValues = new String[] { "1.00803137158",
"1.00751321858", "1.00803137158", "1.00777229508", "1.00803137158",
+ "1.00777229508" };
+
+ // Then
+ for (LoanRepaymentScheduleInstallment period : periods) {
+ BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType,
daysInMonthType, interestRate, period);
+
Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1],
rateFactor.toString());
+ }
+ }
+
+ @Test
+ public void
testFnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual() {
+ // Given
+ final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
+ final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
+ final MathContext mc = MoneyHelper.getMathContext();
+ final String[] expectedValues = new String[] { "1.00000000000",
"2.00753380274", "3.02370122596", "4.04726671069", "5.07986086861",
+ "6.11945121660" };
+
+ final List<BigDecimal> fnValuesCalculated = new ArrayList<>();
+ BigDecimal previousFnValue = BigDecimal.ZERO;
+ for (LoanRepaymentScheduleInstallment period : periods) {
+ BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType,
daysInMonthType, interestRate, period);
+
+ final BigDecimal currentFnValue =
emiCalculator.fnValue(previousFnValue, rateFactor, mc);
+ fnValuesCalculated.add(currentFnValue);
+
+ previousFnValue = currentFnValue;
+ }
+
+ int idx = 0;
+ for (BigDecimal fnValue : fnValuesCalculated) {
+ Assertions.assertEquals(expectedValues[idx++], fnValue.toString());
+ }
+ }
+
+ @Test
+ public void
testEMICalculation_principal100_dayInYearsActual_daysInMonthActual_repayEvery1Month()
{
+ final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2,
LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+ 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)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5,
LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6,
LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final BigDecimal principal = BigDecimal.valueOf(100);
+ final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
+
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
+
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+ final EMICalculationResult result =
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams,
loanProductRelatedDetail,
+ expectedRepaymentPeriods, mc);
+
+ // 17.13
+ Assertions.assertEquals(BigDecimal.valueOf(17.1280789505),
result.getEqualMonthlyInstallmentValue());
+
+ Assertions.assertEquals(BigDecimal.valueOf(0.00803137158),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00751321858),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00803137158),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00777229508),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00803137158),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00777229508),
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ // no more period, no more interest
+ Assertions.assertEquals(BigDecimal.valueOf(0),
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ result.reset();
+ Assertions.assertEquals(BigDecimal.valueOf(0.00803137158),
result.getNextRepaymentPeriodRateFactorMinus1());
+ }
+
+ @Test
+ public void
testEMICalculation_principal10000_dayInYearsActual_daysInMonthActual_repayEvery1Month_DisbursementInPreviousYear()
{
+ final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2023, 12, 12), LocalDate.of(2024, 1, 12)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2,
LocalDate.of(2024, 1, 12), LocalDate.of(2024, 2, 12)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3,
LocalDate.of(2024, 2, 12), LocalDate.of(2024, 3, 12)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4,
LocalDate.of(2024, 3, 12), LocalDate.of(2024, 4, 12)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5,
LocalDate.of(2024, 4, 12), LocalDate.of(2024, 5, 12)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6,
LocalDate.of(2024, 5, 12), LocalDate.of(2024, 6, 12)));
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final BigDecimal principal = BigDecimal.valueOf(10_000);
+ final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
+
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
+
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+ final EMICalculationResult result =
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams,
loanProductRelatedDetail,
+ expectedRepaymentPeriods, mc);
+
+ // 1713.12
+ Assertions.assertEquals(BigDecimal.valueOf(1713.12436983),
result.getEqualMonthlyInstallmentValue());
+
+ Assertions.assertEquals(BigDecimal.valueOf(0.00804485776),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00803137158),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00751321858),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00803137158),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00777229508),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00803137158),
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ // no more period, no more interest
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+ }
+
+ @Test
+ public void
testEMICalculation_principal100_dayInYears365_daysInMonthActual_repayEvery1Month()
{
+ final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2,
LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+ 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)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5,
LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6,
LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final BigDecimal principal = BigDecimal.valueOf(100);
+ final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue());
+
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
+
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+ final EMICalculationResult result =
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams,
loanProductRelatedDetail,
+ expectedRepaymentPeriods, mc);
+
+ // 17.13
+ Assertions.assertEquals(BigDecimal.valueOf(17.1293512777),
result.getEqualMonthlyInstallmentValue());
+
+ Assertions.assertEquals(BigDecimal.valueOf(0.00805337534),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00753380274),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00805337534),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00779358904),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00805337534),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00779358904),
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ // no more period, no more interest
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ result.reset();
+ // check reset
+ Assertions.assertEquals(BigDecimal.valueOf(0.00805337534),
result.getNextRepaymentPeriodRateFactorMinus1());
+ }
+
+ @Test
+ public void
testEMICalculation_principal100_dayInYears360_daysInMonth30_repayEvery1Month() {
+ final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2,
LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+ 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)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5,
LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6,
LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final BigDecimal principal = BigDecimal.valueOf(100);
+ final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).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);
+
+ final EMICalculationResult result =
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams,
loanProductRelatedDetail,
+ expectedRepaymentPeriods, mc);
+
+ // 17.13
+ Assertions.assertEquals(BigDecimal.valueOf(17.1306301273),
result.getEqualMonthlyInstallmentValue());
+
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ // no more period, no more interest
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ result.reset();
+ // check reset
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ }
+
+ @Test
+ public void
testEMICalculation_principal100_dayInYears364_daysInMonthDoesntMatter_repayEvery1Week()
{
+ final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 8)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2,
LocalDate.of(2024, 1, 8), LocalDate.of(2024, 1, 15)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3,
LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 22)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4,
LocalDate.of(2024, 1, 22), LocalDate.of(2024, 1, 29)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5,
LocalDate.of(2024, 1, 29), LocalDate.of(2024, 1, 5)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6,
LocalDate.of(2024, 2, 5), LocalDate.of(2024, 2, 12)));
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final BigDecimal principal = BigDecimal.valueOf(100);
+ final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_364.getValue());
+
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+ final EMICalculationResult result =
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams,
loanProductRelatedDetail,
+ expectedRepaymentPeriods, mc);
+
+ // 16.77
+ Assertions.assertEquals(BigDecimal.valueOf(16.7731989919),
result.getEqualMonthlyInstallmentValue());
+
+ final BigDecimal fixValue = new BigDecimal("0.0018235");
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ // no more period, no more interest
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ result.reset();
+ // check reset
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ }
+
+ @Test
+ public void
testEMICalculation_principal100_dayInYears360_daysInMonthDoesntMatter_repayEvery2Week()
{
+ final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 15)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2,
LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 29)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3,
LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 12)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4,
LocalDate.of(2024, 2, 12), LocalDate.of(2024, 2, 26)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5,
LocalDate.of(2024, 2, 26), LocalDate.of(2024, 3, 11)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6,
LocalDate.of(2024, 3, 11), LocalDate.of(2024, 3, 25)));
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final BigDecimal principal = BigDecimal.valueOf(100);
+ final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
+
+ final EMICalculationResult result =
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams,
loanProductRelatedDetail,
+ expectedRepaymentPeriods, mc);
+
+ // 16.88
+ Assertions.assertEquals(new BigDecimal("16.8824319133"),
result.getEqualMonthlyInstallmentValue());
+
+ final BigDecimal fixValue = new BigDecimal("0.00368752222");
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ // no more period, no more interest
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ result.reset();
+ // check reset
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ }
+
+ @Test
+ public void
testEMICalculation_principal100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days()
{
+ final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2,
LocalDate.of(2024, 1, 16), LocalDate.of(2024, 1, 31)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3,
LocalDate.of(2024, 1, 31), LocalDate.of(2024, 2, 15)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4,
LocalDate.of(2024, 2, 15), LocalDate.of(2024, 3, 1)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5,
LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 16)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6,
LocalDate.of(2024, 3, 16), LocalDate.of(2024, 3, 31)));
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final BigDecimal principal = BigDecimal.valueOf(100);
+ final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.DAYS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15);
+
+ final EMICalculationResult result =
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams,
loanProductRelatedDetail,
+ expectedRepaymentPeriods, mc);
+
+ // 16.90
+ Assertions.assertEquals(new BigDecimal("16.8978941103"),
result.getEqualMonthlyInstallmentValue());
+
+ final BigDecimal fixValue = new BigDecimal("0.00395091667");
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ // no more period, no more interest
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ result.reset();
+ // check reset
+ Assertions.assertEquals(fixValue,
result.getNextRepaymentPeriodRateFactorMinus1());
+ }
+
+ @Test
+ public void
testEMICalculation_Disbursement_DownPayment_Principal100_dayInYears360_daysInMonth30_repayEvery1Month()
{
+ final MathContext mc = MoneyHelper.getMathContext();
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final Money principal = Money.of(monetaryCurrency,
BigDecimal.valueOf(100));
+ final Money downPaymentValue = Money.of(monetaryCurrency,
BigDecimal.valueOf(25));
+ final Money outstandingBalance = principal.minus(downPaymentValue);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).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);
+
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+ expectedRepaymentPeriods
+
.add(LoanScheduleModelDisbursementPeriod.disbursement(LocalDate.of(2024, 1, 1),
principal, BigDecimal.ZERO));
+ expectedRepaymentPeriods
+ .add(LoanScheduleModelDownPaymentPeriod.downPayment(1,
LocalDate.of(2024, 1, 1), downPaymentValue, outstandingBalance));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3,
LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4,
LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+ 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,
+ expectedRepaymentPeriods, mc);
+
+ // 17.13
+ Assertions.assertEquals(new BigDecimal("15.3574482569"),
result.getEqualMonthlyInstallmentValue());
+
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ // no more period, no more interest
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ result.reset();
+ // check reset
+ Assertions.assertEquals(BigDecimal.valueOf(0.00790183333),
result.getNextRepaymentPeriodRateFactorMinus1());
+ }
+
+ @Test
+ public void testEMICalculation_Principal1000_NoInterest_repayEvery1Month()
{
+ final MathContext mc = MoneyHelper.getMathContext();
+
+ final BigDecimal interestRate = BigDecimal.valueOf(0);
+ final Money principal = Money.of(monetaryCurrency,
BigDecimal.valueOf(1000));
+ final Money outstandingBalance = Money.of(monetaryCurrency,
BigDecimal.ZERO);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getPrincipal()).thenReturn(principal);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).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);
+
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2,
LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+ 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,
+ expectedRepaymentPeriods, mc);
+
+ // 17.13
+ Assertions.assertEquals(new BigDecimal("250"),
result.getEqualMonthlyInstallmentValue());
+
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ // no more period, no more interest
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+
+ result.reset();
+ // check reset
+ Assertions.assertEquals(BigDecimal.ZERO,
result.getNextRepaymentPeriodRateFactorMinus1());
+ }
+
+ @Test
+ public void testUnsupportedRepaymentEveryYear() {
+ final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final BigDecimal principal = BigDecimal.valueOf(100);
+ final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.YEARS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+
+ try {
+ emiCalculator.calculateEMIValueAndRateFactors(scheduleParams,
loanProductRelatedDetail, expectedRepaymentPeriods, mc);
+ Assertions.fail();
+ } catch (Exception e) {
+ Assertions.assertInstanceOf(UnsupportedOperationException.class,
e);
+ }
+ }
+
+ @Test
+ public void testUnsupportedRepaymentEveryWholeTerm() {
+ final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final BigDecimal principal = BigDecimal.valueOf(100);
+ final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WHOLE_TERM);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+
+ try {
+ emiCalculator.calculateEMIValueAndRateFactors(scheduleParams,
loanProductRelatedDetail, expectedRepaymentPeriods, mc);
+ Assertions.fail();
+ } catch (Exception e) {
+ Assertions.assertInstanceOf(UnsupportedOperationException.class,
e);
+ }
+ }
+
+ @Test
+ public void testInvalidRepaymentEveryValue() {
+ final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new
ArrayList<>();
+
+ final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ final BigDecimal principal = BigDecimal.valueOf(100);
+ final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.INVALID);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+ expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1,
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+
+ try {
+ emiCalculator.calculateEMIValueAndRateFactors(scheduleParams,
loanProductRelatedDetail, expectedRepaymentPeriods, mc);
+ Assertions.fail();
+ } catch (Exception e) {
+ Assertions.assertInstanceOf(UnsupportedOperationException.class,
e);
+ }
+ }
+
+ @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;
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
index 7d3685d7b..1d2e9f487 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
@@ -20,6 +20,8 @@ package
org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
import lombok.extern.slf4j.Slf4j;
import net.fortuna.ical4j.model.Recur;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -40,6 +42,32 @@ import org.springframework.stereotype.Component;
@Slf4j
public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
+ @Override
+ public List<PreGeneratedLoanSchedulePeriod> generateRepaymentPeriods(final
LoanApplicationTerms loanApplicationTerms,
+ final HolidayDetailDTO holidayDetailDTO) {
+ final int numberOfRepayments =
loanApplicationTerms.getNumberOfRepayments();
+ final ArrayList<PreGeneratedLoanSchedulePeriod> repaymentPeriods = new
ArrayList<>(numberOfRepayments);
+ // TODO: check periodStartDate
+ LocalDate lastRepaymentDate =
loanApplicationTerms.getRepaymentStartDate();
+ LocalDate nextRepaymentDate;
+ boolean isFirstRepayment = true;
+ for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments;
repaymentPeriod++) {
+ nextRepaymentDate = generateNextRepaymentDate(lastRepaymentDate,
loanApplicationTerms, isFirstRepayment);
+
+ if (repaymentPeriod == numberOfRepayments) { // last period
+ if (loanApplicationTerms.getFixedLength() != null
+ &&
loanApplicationTerms.calculateMaxDateForFixedLength().compareTo(nextRepaymentDate)
!= 0) {
+ nextRepaymentDate =
loanApplicationTerms.calculateMaxDateForFixedLength();
+ }
+ nextRepaymentDate = adjustRepaymentDate(nextRepaymentDate,
loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate();
+ }
+ repaymentPeriods.add(new
PreGeneratedLoanSchedulePeriod(repaymentPeriod, lastRepaymentDate,
nextRepaymentDate));
+ lastRepaymentDate = nextRepaymentDate;
+ isFirstRepayment = false;
+ }
+ return repaymentPeriods;
+ }
+
@Override
public LocalDate getLastRepaymentDate(final LoanApplicationTerms
loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO) {
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
index 10d966e4d..5b6987bea 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
@@ -38,6 +38,7 @@ import static org.mockito.BDDMockito.given;
import java.math.BigDecimal;
import java.time.LocalDate;
+import java.util.List;
import
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.junit.context.WithTenantContext;
import org.apache.fineract.junit.context.WithTenantContextExtension;
@@ -74,6 +75,47 @@ public class DefaultScheduledDateGeneratorTest {
moneyHelper.initialize();
}
+ @Test
+ @WithSystemTimeZone(EUROPE_BERLIN_ID)
+ @WithTenantContext(tenantTimeZoneId = EUROPE_BERLIN_ID)
+ @WithSystemProperty(key = FLOATING_TIMEZONE_PROPERTY_KEY, value = "true")
+ public void test_generateRepaymentPeriods() {
+ // given
+ HolidayDetailDTO holidayDetailDTO = createHolidayDTO();
+
+ ApplicationCurrency dollarCurrency = new ApplicationCurrency("USD",
"US Dollar", 2, 0, "currency.USD", "$");
+ Money principalAmount =
Money.of(fromApplicationCurrency(dollarCurrency), BigDecimal.valueOf(100));
+ LocalDate expectedDisbursementDate = LocalDate.of(2024, 1, 1);
+ LocalDate dueRepaymentPeriodDate = LocalDate.of(2024, 2, 1);
+
+ LocalDate submittedOnDate = LocalDate.of(2024, 1, 1);
+ LoanApplicationTerms loanApplicationTerms =
LoanApplicationTerms.assembleFrom(dollarCurrency, 1, MONTHS, 4, 1, MONTHS, null,
+ INVALID, EQUAL_PRINCIPAL, FLAT, ZERO, MONTHS, ZERO,
SAME_AS_REPAYMENT_PERIOD, false, principalAmount,
+ expectedDisbursementDate, null, dueRepaymentPeriodDate, null,
null, null, null, null,
+ Money.of(fromApplicationCurrency(dollarCurrency), ZERO),
false, null, EMPTY_LIST, BigDecimal.valueOf(36_000L), null,
+ DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null,
null, null, null, null, ZERO, null, NONE, null, ZERO,
+ EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false,
false, null, false, false, null, false, DISBURSEMENT_DATE,
+ submittedOnDate, CUMULATIVE,
LoanScheduleProcessingType.HORIZONTAL, null);
+
+ // when
+ List<PreGeneratedLoanSchedulePeriod> result =
underTest.generateRepaymentPeriods(loanApplicationTerms, holidayDetailDTO);
+
+ // then
+ assertThat(result.size()).isEqualTo(4);
+ assertThat(result.get(0).periodNumber()).isEqualTo(1);
+
assertThat(result.get(0).periodFromDate().toString()).isEqualTo("2024-01-01");
+
assertThat(result.get(0).periodDueDate().toString()).isEqualTo("2024-02-01");
+ assertThat(result.get(1).periodNumber()).isEqualTo(2);
+
assertThat(result.get(1).periodFromDate().toString()).isEqualTo("2024-02-01");
+
assertThat(result.get(1).periodDueDate().toString()).isEqualTo("2024-03-01");
+ assertThat(result.get(2).periodNumber()).isEqualTo(3);
+
assertThat(result.get(2).periodFromDate().toString()).isEqualTo("2024-03-01");
+
assertThat(result.get(2).periodDueDate().toString()).isEqualTo("2024-04-01");
+ assertThat(result.get(3).periodNumber()).isEqualTo(4);
+
assertThat(result.get(3).periodFromDate().toString()).isEqualTo("2024-04-01");
+
assertThat(result.get(3).periodDueDate().toString()).isEqualTo("2024-05-01");
+ }
+
@Test
@WithSystemTimeZone(EUROPE_BERLIN_ID)
@WithTenantContext(tenantTimeZoneId = EUROPE_BERLIN_ID)