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
+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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user