FINERACT-1981: EMI Calculation, Daily, Weekly, Monthly support plug to Repayment Schedule

This commit is contained in:
Janos Meszaros
2024-06-26 09:21:38 +02:00
committed by Adam Saghy
parent 9f5044b358
commit db8000b4f3
15 changed files with 1312 additions and 328 deletions
@@ -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,
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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);
}
}