blob: 5a8af0483493985750f244e7c7cb4918b43220f2 [file] [log] [blame]
/**
* 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.math.MathContext;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
/**
* <p>
* Declining balance can be amortized (see {@link AmortizationMethod}) in two ways at present:
* <ol>
* <li>Equal principal payments</li>
* <li>Equal installment payments</li>
* </ol>
* <p>
* </p>
*
* <p>
* When amortized using <i>equal principal payments</i>, the <b>principal component</b> of each installment is fixed and
* <b>interest due</b> is calculated from the <b>outstanding principal balance</b> resulting in a different <b>total
* payment due</b> for each installment.
* </p>
*
* <p>
* When amortized using <i>equal installments</i>, the <b>total payment due</b> for each installment is fixed and is
* calculated using the excel like <code>pmt</code> function. The <b>interest due</b> is calculated from the
* <b>outstanding principal balance</b> which results in a <b>principal component</b> that is <b>total payment due</b>
* minus <b>interest due</b>.
* </p>
*/
public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanScheduleGenerator {
@Override
public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final PaymentPeriodsInOneYearCalculator calculator,
final double interestCalculationGraceOnRepaymentPeriodFraction, final Money totalCumulativePrincipal,
@SuppressWarnings("unused") final Money totalCumulativeInterest,
@SuppressWarnings("unused") final Money totalInterestDueForLoan, final Money cumulatingInterestPaymentDueToGrace,
final Money outstandingBalance, final LoanApplicationTerms loanApplicationTerms, int periodNumber, final MathContext mc,
final TreeMap<LocalDate, Money> principalVariation, final Map<LocalDate, Money> compoundingMap, final LocalDate periodStartDate,
final LocalDate periodEndDate, final Collection<LoanTermVariationsData> termVariations) {
LocalDate interestStartDate = periodStartDate;
Money interestForThisInstallment = totalCumulativePrincipal.zero();
Money compoundedInterest = totalCumulativePrincipal.zero();
Money balanceForInterestCalculation = outstandingBalance;
Money cumulatingInterestDueToGrace = cumulatingInterestPaymentDueToGrace;
Map<LocalDate, BigDecimal> interestRates = new HashMap<>(termVariations.size());
for (LoanTermVariationsData loanTermVariation : termVariations) {
if (loanTermVariation.getTermVariationType().isInterestRateVariation()
&& loanTermVariation.isApplicable(periodStartDate, periodEndDate)) {
LocalDate fromDate = loanTermVariation.getTermApplicableFrom();
if (fromDate == null) {
fromDate = periodStartDate;
}
interestRates.put(fromDate, loanTermVariation.getDecimalValue());
if (!principalVariation.containsKey(fromDate)) {
principalVariation.put(fromDate, balanceForInterestCalculation.zero());
}
}
}
if (principalVariation != null) {
for (Map.Entry<LocalDate, Money> principal : principalVariation.entrySet()) {
if (!principal.getKey().isAfter(periodEndDate)) {
int interestForDays = Math.toIntExact(ChronoUnit.DAYS.between(interestStartDate, principal.getKey()));
if (interestForDays > 0) {
final PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator,
interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestDueToGrace,
balanceForInterestCalculation, interestStartDate, principal.getKey());
interestForThisInstallment = interestForThisInstallment.plus(result.interest());
cumulatingInterestDueToGrace = result.interestPaymentDueToGrace();
interestStartDate = principal.getKey();
}
Money compoundFee = totalCumulativePrincipal.zero();
if (compoundingMap.containsKey(principal.getKey())) {
Money interestToBeCompounded = totalCumulativePrincipal.zero();
// for interest compounding
if (loanApplicationTerms.getInterestRecalculationCompoundingMethod().isInterestCompoundingEnabled()) {
interestToBeCompounded = interestForThisInstallment.minus(compoundedInterest);
balanceForInterestCalculation = balanceForInterestCalculation.plus(interestToBeCompounded);
compoundedInterest = interestForThisInstallment;
}
// fee compounding will be done after calculation
compoundFee = compoundingMap.get(principal.getKey());
compoundingMap.put(principal.getKey(), interestToBeCompounded.plus(compoundFee));
}
if (!loanApplicationTerms.isPrincipalCompoundingDisabledForOverdueLoans()) {
balanceForInterestCalculation = balanceForInterestCalculation.plus(principal.getValue());
}
balanceForInterestCalculation = balanceForInterestCalculation.plus(compoundFee);
if (interestRates.containsKey(principal.getKey())) {
loanApplicationTerms.updateAnnualNominalInterestRate(interestRates.get(principal.getKey()));
}
}
}
}
final PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator,
interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestDueToGrace,
balanceForInterestCalculation, interestStartDate, periodEndDate);
interestForThisInstallment = interestForThisInstallment.plus(result.interest());
cumulatingInterestDueToGrace = result.interestPaymentDueToGrace();
if (loanApplicationTerms.isInterestToBeRecoveredFirstWhenGreaterThanEMIEnabled()
&& loanApplicationTerms.isInterestTobeApproppriated()) {
interestForThisInstallment = interestForThisInstallment.add(loanApplicationTerms.getInterestTobeApproppriated());
loanApplicationTerms.setInterestTobeApproppriated(interestForThisInstallment.zero());
}
Money interestForPeriod = interestForThisInstallment;
if (interestForPeriod.isGreaterThanZero()) {
interestForPeriod = interestForPeriod.minus(cumulatingInterestPaymentDueToGrace);
} else {
interestForPeriod = cumulatingInterestDueToGrace.minus(cumulatingInterestPaymentDueToGrace);
}
Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, outstandingBalance,
periodNumber, mc, interestForPeriod);
if (loanApplicationTerms.isInterestToBeRecoveredFirstWhenGreaterThanEMIEnabled() && principalForThisInstallment.isLessThanZero()
&& !loanApplicationTerms.isLastRepaymentPeriod(periodNumber)) {
loanApplicationTerms.setInterestTobeApproppriated(principalForThisInstallment.abs());
interestForThisInstallment = interestForThisInstallment.minus(loanApplicationTerms.getInterestTobeApproppriated());
principalForThisInstallment = principalForThisInstallment.zero();
}
// update cumulative fields for principal & interest
final Money interestBroughtFowardDueToGrace = cumulatingInterestDueToGrace;
final Money totalCumulativePrincipalToDate = totalCumulativePrincipal.plus(principalForThisInstallment);
// adjust if needed
principalForThisInstallment = loanApplicationTerms.adjustPrincipalIfLastRepaymentPeriod(principalForThisInstallment,
totalCumulativePrincipalToDate, periodNumber);
PrincipalInterest principalInterest = new PrincipalInterest(principalForThisInstallment, interestForThisInstallment,
interestBroughtFowardDueToGrace);
principalInterest.setRescheduleInterestPortion(loanApplicationTerms.getInterestTobeApproppriated());
return principalInterest;
}
}