| /** |
| * 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; |
| } |
| } |