FINERACT-1981: EMI Calculation, Daily, Weekly, Monthly support plug to Repayment Schedule
This commit is contained in:
committed by
Adam Saghy
parent
9f5044b358
commit
db8000b4f3
-10
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+96
@@ -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();
|
||||
}
|
||||
}
|
||||
+4
@@ -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,
|
||||
|
||||
+12
-3
@@ -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,
|
||||
|
||||
+29
-24
@@ -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;
|
||||
}
|
||||
|
||||
return new PrincipalInterest(calculatedPrincipal, Money.zero(loanApplicationTerms.getCurrency()),
|
||||
Money.zero(loanApplicationTerms.getCurrency()));
|
||||
@Override
|
||||
public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final LoanApplicationTerms loanApplicationTerms,
|
||||
final LoanScheduleParams loanScheduleParams, final EMICalculationResult emiCalculationResult, final MathContext mc) {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-7
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-125
@@ -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);
|
||||
}
|
||||
|
||||
+437
@@ -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);
|
||||
}
|
||||
}
|
||||
-159
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+621
@@ -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;
|
||||
}
|
||||
}
|
||||
+28
@@ -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) {
|
||||
|
||||
|
||||
+42
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user