blob: f53a50ef5e193f1b929f369115d239e465d46be4 [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.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.joda.time.Days;
import org.joda.time.LocalDate;
/**
* <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>
* 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, final 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 compoundedMoney = 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) {
// identifies rest date after current date for reducing all
// compounding
// values
LocalDate compoundingEndDate = principalVariation.ceilingKey(DateUtils.getLocalDateOfTenant());
if (compoundingEndDate == null) {
compoundingEndDate = DateUtils.getLocalDateOfTenant();
}
for (Map.Entry<LocalDate, Money> principal : principalVariation.entrySet()) {
if (!principal.getKey().isAfter(periodEndDate)) {
int interestForDays = Days.daysBetween(interestStartDate, principal.getKey()).getDays();
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());
compoundedMoney = compoundedMoney.plus(interestToBeCompounded).plus(compoundFee);
}
balanceForInterestCalculation = balanceForInterestCalculation.plus(principal.getValue()).plus(compoundFee);
if (interestRates.containsKey(principal.getKey())) {
loanApplicationTerms.updateAnnualNominalInterestRate(interestRates.get(principal.getKey()));
}
}
}
if (!periodEndDate.isBefore(compoundingEndDate)) {
balanceForInterestCalculation = balanceForInterestCalculation.minus(compoundedMoney);
compoundingMap.clear();
} else if (compoundedMoney.isGreaterThanZero()) {
compoundingMap.put(periodEndDate, compoundedMoney);
compoundingMap.put(compoundingEndDate, compoundedMoney.negated());
clearMapDetails(periodEndDate, compoundingMap);
}
}
final PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator,
interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestDueToGrace,
balanceForInterestCalculation, interestStartDate, periodEndDate);
interestForThisInstallment = interestForThisInstallment.plus(result.interest());
cumulatingInterestDueToGrace = result.interestPaymentDueToGrace();
Money interestForPeriod = interestForThisInstallment;
if (interestForPeriod.isGreaterThanZero()) {
interestForPeriod = interestForPeriod.minus(cumulatingInterestPaymentDueToGrace);
} else {
interestForPeriod = cumulatingInterestDueToGrace.minus(cumulatingInterestPaymentDueToGrace);
}
Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, outstandingBalance,
periodNumber, mc, interestForPeriod);
// 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);
return new PrincipalInterest(principalForThisInstallment, interestForThisInstallment, interestBroughtFowardDueToGrace);
}
}