blob: 32f855433d606bce6192e7d5e5656e45544a53c8 [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 jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.calendar.data.CalendarHistoryDataWrapper;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.common.domain.DayOfWeekType;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.NthDayType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
@Slf4j
public final class LoanApplicationTerms {
private final ApplicationCurrency currency;
private final Calendar loanCalendar;
private Integer loanTermFrequency;
private final PeriodFrequencyType loanTermPeriodFrequencyType;
private Integer numberOfRepayments;
private Integer actualNumberOfRepayments;
private final Integer repaymentEvery;
private final PeriodFrequencyType repaymentPeriodFrequencyType;
private final Integer fixedLength;
private final Integer nthDay;
private final DayOfWeekType weekDayType;
private final AmortizationMethod amortizationMethod;
private final InterestMethod interestMethod;
private BigDecimal interestRatePerPeriod;
private final PeriodFrequencyType interestRatePeriodFrequencyType;
private BigDecimal annualNominalInterestRate;
private final InterestCalculationPeriodMethod interestCalculationPeriodMethod;
private final boolean allowPartialPeriodInterestCalcualtion;
private Money principal;
private final LocalDate expectedDisbursementDate;
private final LocalDate repaymentsStartingFromDate;
private final LocalDate calculatedRepaymentsStartingFromDate;
/**
* Integer representing the number of 'repayment frequencies' or installments where 'grace' should apply to the
* principal component of a loans repayment period (installment).
*/
private Integer principalGrace;
private Integer recurringMoratoriumOnPrincipalPeriods;
/**
* Integer representing the number of 'repayment frequencies' or installments where 'grace' should apply to the
* payment of interest in a loans repayment period (installment).
*
* <b>Note:</b> Interest is still calculated taking into account the full loan term, the interest is simply offset
* to a later period.
*/
private Integer interestPaymentGrace;
/**
* Integer representing the number of 'repayment frequencies' or installments where 'grace' should apply to the
* charging of interest in a loans repayment period (installment).
*
* <b>Note:</b> The loan is <i>interest-free</i> for the period of time indicated.
*/
private final Integer interestChargingGrace;
/**
* Legacy method of support 'grace' on the charging of interest on a loan.
*
* <p>
* For the typical structured loan, its reasonable to use an integer to indicate the number of 'repayment frequency'
* periods the 'grace' should apply to but for slightly <b>irregular</b> loans where the period between disbursement
* and the date of the 'first repayment period' isnt doest match the 'repayment frequency' but can be less (15days
* instead of 1 month) or more (6 weeks instead of 1 month) - The idea was to use a date to indicate from whence
* interest should be charged.
* </p>
*/
private LocalDate interestChargedFromDate;
private final Money inArrearsTolerance;
private final Integer graceOnArrearsAgeing;
// added
private LocalDate loanEndDate;
private final List<DisbursementData> disbursementDatas;
private final boolean multiDisburseLoan;
private BigDecimal fixedEmiAmount;
private BigDecimal fixedPrincipalAmount;
private BigDecimal currentPeriodFixedEmiAmount;
private BigDecimal currentPeriodFixedPrincipalAmount;
private final BigDecimal actualFixedEmiAmount;
private final BigDecimal maxOutstandingBalance;
private Money totalInterestDue;
private final DaysInMonthType daysInMonthType;
private final DaysInYearType daysInYearType;
private final boolean interestRecalculationEnabled;
private final LoanRescheduleStrategyMethod rescheduleStrategyMethod;
private final InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod;
private final CalendarInstance restCalendarInstance;
private final RecalculationFrequencyType recalculationFrequencyType;
private final CalendarInstance compoundingCalendarInstance;
private final RecalculationFrequencyType compoundingFrequencyType;
private final boolean allowCompoundingOnEod;
private final BigDecimal principalThresholdForLastInstalment;
private final Integer installmentAmountInMultiplesOf;
private final LoanPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy;
private Money approvedPrincipal = null;
private final LoanTermVariationsDataWrapper variationsDataWrapper;
private Money adjustPrincipalForFlatLoans;
private LocalDate seedDate;
private final CalendarHistoryDataWrapper calendarHistoryDataWrapper;
private final Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled;
private final Integer numberOfDays;
private final boolean isSkipRepaymentOnFirstDayOfMonth;
private final boolean isFirstRepaymentDateAllowedOnHoliday;
private final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI;
private boolean isPrincipalCompoundingDisabledForOverdueLoans;
private final HolidayDetailDTO holidayDetailDTO;
private final Set<Integer> periodNumbersApplicableForPrincipalGrace = new HashSet<>();
private final Set<Integer> periodNumbersApplicableForInterestGrace = new HashSet<>();
// used for FLAT loans when interest rate changed
private Integer excludePeriodsForCalculation = 0;
private Money totalPrincipalAccountedForInterestCalcualtion;
// used for FLAT loans generation on modifying terms
private Money totalPrincipalAccounted;
private Money totalInterestAccounted;
private int periodsCompleted = 0;
private int extraPeriods = 0;
private boolean isEqualAmortization;
private Money interestTobeApproppriated;
private final BigDecimal fixedPrincipalPercentagePerInstallment;
private LocalDate newScheduledDueDateStart;
private boolean isDownPaymentEnabled;
private BigDecimal disbursedAmountPercentageForDownPayment;
private Money downPaymentAmount;
private boolean isAutoRepaymentForDownPaymentEnabled;
private RepaymentStartDateType repaymentStartDateType;
private LocalDate submittedOnDate;
private boolean isScheduleExtensionForDownPaymentDisabled;
private Money disbursedPrincipal;
private final LoanScheduleType loanScheduleType;
private final LoanScheduleProcessingType loanScheduleProcessingType;
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
final PeriodFrequencyType repaymentPeriodFrequencyType, Integer nthDay, DayOfWeekType weekDayType,
final AmortizationMethod amortizationMethod, final InterestMethod interestMethod, final BigDecimal interestRatePerPeriod,
final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualNominalInterestRate,
final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final boolean allowPartialPeriodInterestCalcualtion,
final Money principalMoney, final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate,
final LocalDate calculatedRepaymentsStartingFromDate, final Integer graceOnPrincipalPayment,
final Integer recurringMoratoriumOnPrincipalPeriods, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged,
final LocalDate interestChargedFromDate, final Money inArrearsTolerance, final boolean multiDisburseLoan,
final BigDecimal emiAmount, final List<DisbursementData> disbursementDatas, final BigDecimal maxOutstandingBalance,
final Integer graceOnArrearsAgeing, final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType,
final boolean isInterestRecalculationEnabled, final RecalculationFrequencyType recalculationFrequencyType,
final CalendarInstance restCalendarInstance,
final InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod,
final CalendarInstance compoundingCalendarInstance, final RecalculationFrequencyType compoundingFrequencyType,
final BigDecimal principalThresholdForLastInstalment, final Integer installmentAmountInMultiplesOf,
final LoanPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy, final Calendar loanCalendar,
BigDecimal approvedAmount, List<LoanTermVariationsData> loanTermVariations,
Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final Integer numberOfDays,
boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod,
final boolean isEqualAmortization, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI,
final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans,
final Boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
final Boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType,
final LocalDate submittedOnDate, final LoanScheduleType loanScheduleType,
final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength) {
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
return new LoanApplicationTerms(currency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery,
repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod, interestRatePerPeriod,
interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
allowPartialPeriodInterestCalcualtion, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate,
calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods,
graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount,
disbursementDatas, maxOutstandingBalance, graceOnArrearsAgeing, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, rescheduleStrategyMethod, interestRecalculationCompoundingMethod, restCalendarInstance,
recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment,
installmentAmountInMultiplesOf, preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations,
calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays,
isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false,
isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment,
isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType,
fixedLength);
}
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, NthDayType nthDay, DayOfWeekType dayOfWeek,
final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate,
final LocalDate calculatedRepaymentsStartingFromDate, final Money inArrearsTolerance,
final LoanProductRelatedDetail loanProductRelatedDetail, final boolean multiDisburseLoan, final BigDecimal emiAmount,
final List<DisbursementData> disbursementDatas, final BigDecimal maxOutstandingBalance, final LocalDate interestChargedFromDate,
final BigDecimal principalThresholdForLastInstalment, final Integer installmentAmountInMultiplesOf,
final RecalculationFrequencyType recalculationFrequencyType, final CalendarInstance restCalendarInstance,
final InterestRecalculationCompoundingMethod compoundingMethod, final CalendarInstance compoundingCalendarInstance,
final RecalculationFrequencyType compoundingFrequencyType,
final LoanPreClosureInterestCalculationStrategy loanPreClosureInterestCalculationStrategy,
final LoanRescheduleStrategyMethod rescheduleStrategyMethod, final Calendar loanCalendar, BigDecimal approvedAmount,
BigDecimal annualNominalInterestRate, final List<LoanTermVariationsData> loanTermVariations,
final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Integer numberOfDays,
final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod,
final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI,
final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans,
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate) {
final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments();
final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery();
final PeriodFrequencyType repaymentPeriodFrequencyType = loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
final AmortizationMethod amortizationMethod = loanProductRelatedDetail.getAmortizationMethod();
final InterestMethod interestMethod = loanProductRelatedDetail.getInterestMethod();
final BigDecimal interestRatePerPeriod = loanProductRelatedDetail.getNominalInterestRatePerPeriod();
final PeriodFrequencyType interestRatePeriodFrequencyType = loanProductRelatedDetail.getInterestPeriodFrequencyType();
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = loanProductRelatedDetail
.getInterestCalculationPeriodMethod();
final boolean allowPartialPeriodInterestCalcualtion = loanProductRelatedDetail.isAllowPartialPeriodInterestCalcualtion();
final Money principalMoney = loanProductRelatedDetail.getPrincipal();
//
final Integer graceOnPrincipalPayment = loanProductRelatedDetail.graceOnPrincipalPayment();
final Integer recurringMoratoriumOnPrincipalPeriods = loanProductRelatedDetail.recurringMoratoriumOnPrincipalPeriods();
final Integer graceOnInterestPayment = loanProductRelatedDetail.graceOnInterestPayment();
final Integer graceOnInterestCharged = loanProductRelatedDetail.graceOnInterestCharged();
// Interest recalculation settings
final DaysInMonthType daysInMonthType = loanProductRelatedDetail.fetchDaysInMonthType();
final DaysInYearType daysInYearType = loanProductRelatedDetail.fetchDaysInYearType();
final boolean isInterestRecalculationEnabled = loanProductRelatedDetail.isInterestRecalculationEnabled();
final boolean isInterestChargedFromDateSameAsDisbursalDateEnabled = false;
final boolean isEqualAmortization = loanProductRelatedDetail.isEqualAmortization();
final boolean isDownPaymentEnabled = loanProductRelatedDetail.isEnableDownPayment();
BigDecimal disbursedAmountPercentageForDownPayment = null;
boolean isAutoRepaymentForDownPaymentEnabled = false;
if (isDownPaymentEnabled) {
disbursedAmountPercentageForDownPayment = loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment();
isAutoRepaymentForDownPaymentEnabled = loanProductRelatedDetail.isEnableAutoRepaymentForDownPayment();
}
LoanScheduleType loanScheduleType = loanProductRelatedDetail.getLoanScheduleType();
LoanScheduleProcessingType loanScheduleProcessingType = loanProductRelatedDetail.getLoanScheduleProcessingType();
final Integer fixedLength = loanProductRelatedDetail.getFixedLength();
return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
repaymentEvery, repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod,
interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate,
interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion, principalMoney, expectedDisbursementDate,
repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment,
recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate,
inArrearsTolerance, multiDisburseLoan, emiAmount, disbursementDatas, maxOutstandingBalance,
loanProductRelatedDetail.getGraceOnDueDate(), daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
rescheduleStrategyMethod, compoundingMethod, restCalendarInstance, recalculationFrequencyType, compoundingCalendarInstance,
compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf,
loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper,
isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO,
allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday,
isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment,
isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType,
fixedLength);
}
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer nthDay, final DayOfWeekType weekDayType,
final AmortizationMethod amortizationMethod, final InterestMethod interestMethod, final BigDecimal interestRatePerPeriod,
final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualNominalInterestRate,
final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final boolean allowPartialPeriodInterestCalcualtion,
final Money principal, final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate,
final LocalDate calculatedRepaymentsStartingFromDate, final Integer principalGrace,
final Integer recurringMoratoriumOnPrincipalPeriods, final Integer interestPaymentGrace, final Integer interestChargingGrace,
final LocalDate interestChargedFromDate, final Money inArrearsTolerance, final boolean multiDisburseLoan,
final BigDecimal emiAmount, final List<DisbursementData> disbursementDatas, final BigDecimal maxOutstandingBalance,
final Integer graceOnArrearsAgeing, final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType,
final boolean isInterestRecalculationEnabled, final LoanRescheduleStrategyMethod rescheduleStrategyMethod,
final InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod,
final CalendarInstance restCalendarInstance, final RecalculationFrequencyType recalculationFrequencyType,
final CalendarInstance compoundingCalendarInstance, final RecalculationFrequencyType compoundingFrequencyType,
final BigDecimal principalThresholdForLastInstalment, final Integer installmentAmountInMultiplesOf,
final LoanPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy, final Calendar loanCalendar,
BigDecimal approvedAmount, List<LoanTermVariationsData> loanTermVariations,
final CalendarHistoryDataWrapper calendarHistoryDataWrapper, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled,
final Integer numberOfDays, final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO,
final boolean allowCompoundingOnEod, final boolean isEqualAmortization, final boolean isFirstRepaymentDateAllowedOnHoliday,
final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, final BigDecimal fixedPrincipalPercentagePerInstallment,
final boolean isPrincipalCompoundingDisabledForOverdueLoans, final boolean isDownPaymentEnabled,
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled,
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, final LoanScheduleType loanScheduleType,
final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength) {
this.currency = currency;
this.loanTermFrequency = loanTermFrequency;
this.loanTermPeriodFrequencyType = loanTermPeriodFrequencyType;
this.numberOfRepayments = numberOfRepayments;
this.repaymentEvery = repaymentEvery;
this.repaymentPeriodFrequencyType = repaymentPeriodFrequencyType;
this.nthDay = nthDay;
this.weekDayType = weekDayType;
this.amortizationMethod = amortizationMethod;
this.interestMethod = interestMethod;
this.interestRatePerPeriod = interestRatePerPeriod;
this.interestRatePeriodFrequencyType = interestRatePeriodFrequencyType;
this.annualNominalInterestRate = annualNominalInterestRate;
this.interestCalculationPeriodMethod = interestCalculationPeriodMethod;
this.allowPartialPeriodInterestCalcualtion = allowPartialPeriodInterestCalcualtion;
this.principal = principal;
this.disbursedPrincipal = principal;
this.expectedDisbursementDate = expectedDisbursementDate;
this.repaymentsStartingFromDate = repaymentsStartingFromDate;
this.calculatedRepaymentsStartingFromDate = calculatedRepaymentsStartingFromDate;
this.principalGrace = principalGrace;
this.recurringMoratoriumOnPrincipalPeriods = recurringMoratoriumOnPrincipalPeriods;
this.interestPaymentGrace = interestPaymentGrace;
this.interestChargingGrace = interestChargingGrace;
this.interestChargedFromDate = interestChargedFromDate;
this.inArrearsTolerance = inArrearsTolerance;
this.multiDisburseLoan = multiDisburseLoan;
this.fixedEmiAmount = emiAmount;
this.actualFixedEmiAmount = emiAmount;
this.disbursementDatas = disbursementDatas;
this.maxOutstandingBalance = maxOutstandingBalance;
this.graceOnArrearsAgeing = graceOnArrearsAgeing;
this.daysInMonthType = daysInMonthType;
this.daysInYearType = daysInYearType;
this.interestRecalculationEnabled = isInterestRecalculationEnabled;
this.rescheduleStrategyMethod = rescheduleStrategyMethod;
this.interestRecalculationCompoundingMethod = interestRecalculationCompoundingMethod;
this.restCalendarInstance = restCalendarInstance;
this.compoundingCalendarInstance = compoundingCalendarInstance;
this.recalculationFrequencyType = recalculationFrequencyType;
this.compoundingFrequencyType = compoundingFrequencyType;
this.principalThresholdForLastInstalment = principalThresholdForLastInstalment;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
this.preClosureInterestCalculationStrategy = preClosureInterestCalculationStrategy;
this.isSkipRepaymentOnFirstDayOfMonth = isSkipRepaymentOnFirstDayOfMonth;
this.numberOfDays = numberOfDays;
this.loanCalendar = loanCalendar;
this.approvedPrincipal = Money.of(principal.getCurrency(), approvedAmount);
this.variationsDataWrapper = new LoanTermVariationsDataWrapper(loanTermVariations);
this.actualNumberOfRepayments = numberOfRepayments + getLoanTermVariations().adjustNumberOfRepayments();
this.adjustPrincipalForFlatLoans = principal.zero();
if (this.calculatedRepaymentsStartingFromDate == null) {
this.seedDate = this.expectedDisbursementDate;
} else {
this.seedDate = this.calculatedRepaymentsStartingFromDate;
}
this.calendarHistoryDataWrapper = calendarHistoryDataWrapper;
this.isInterestChargedFromDateSameAsDisbursalDateEnabled = isInterestChargedFromDateSameAsDisbursalDateEnabled;
this.holidayDetailDTO = holidayDetailDTO;
this.allowCompoundingOnEod = allowCompoundingOnEod;
Integer periodNumber = 1;
updatePeriodNumberApplicableForPrincipalOrInterestGrace(periodNumber);
updateRecurringMoratoriumOnPrincipalPeriods(periodNumber);
this.totalPrincipalAccountedForInterestCalcualtion = principal.zero();
this.totalInterestAccounted = principal.zero();
this.totalPrincipalAccounted = principal.zero();
this.isEqualAmortization = isEqualAmortization;
this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday;
this.isInterestToBeRecoveredFirstWhenGreaterThanEMI = isInterestToBeRecoveredFirstWhenGreaterThanEMI;
this.fixedPrincipalPercentagePerInstallment = fixedPrincipalPercentagePerInstallment;
this.isPrincipalCompoundingDisabledForOverdueLoans = isPrincipalCompoundingDisabledForOverdueLoans;
this.isDownPaymentEnabled = isDownPaymentEnabled;
this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment;
this.downPaymentAmount = Money.zero(getCurrency());
if (isDownPaymentEnabled) {
this.downPaymentAmount = Money.of(getCurrency(),
MathUtil.percentageOf(getPrincipal().getAmount(), getDisbursedAmountPercentageForDownPayment(), 19));
if (getInstallmentAmountInMultiplesOf() != null) {
downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount, getInstallmentAmountInMultiplesOf());
}
}
this.isAutoRepaymentForDownPaymentEnabled = isAutoRepaymentForDownPaymentEnabled;
this.repaymentStartDateType = repaymentStartDateType;
this.submittedOnDate = submittedOnDate;
this.loanScheduleType = loanScheduleType;
this.loanScheduleProcessingType = loanScheduleProcessingType;
this.fixedLength = fixedLength;
}
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
final int periodNumber) {
Money adjusted = principalForPeriod;
final Money totalPrincipalRemaining = this.principal.minus(totalCumulativePrincipalToDate);
if (totalPrincipalRemaining.isLessThanZero()) {
// paid too much principal, subtract amount that overpays from
// principal paid for period.
adjusted = principalForPeriod.minus(totalPrincipalRemaining.abs());
} else if (this.actualFixedEmiAmount != null) {
final Money difference = this.principal.minus(totalCumulativePrincipalToDate);
final Money principalThreshold = principalForPeriod.multipliedBy(this.principalThresholdForLastInstalment).dividedBy(100,
MoneyHelper.getRoundingMode());
if (difference.isLessThan(principalThreshold)) {
adjusted = principalForPeriod.plus(difference.abs());
}
} else if (isLastRepaymentPeriod(this.actualNumberOfRepayments, periodNumber)) {
final Money difference = totalCumulativePrincipalToDate.minus(this.principal);
if (difference.isLessThanZero()) {
adjusted = principalForPeriod.plus(difference.abs());
} else if (difference.isGreaterThanZero()) {
adjusted = principalForPeriod.minus(difference.abs());
}
}
return adjusted;
}
public Money adjustInterestIfLastRepaymentPeriod(final Money interestForThisPeriod, final Money totalCumulativeInterestToDate,
final Money totalInterestDueForLoan, final int periodNumber) {
Money adjusted = interestForThisPeriod;
final Money totalInterestRemaining = totalInterestDueForLoan.minus(totalCumulativeInterestToDate);
if (totalInterestRemaining.isLessThanZero()) {
// paid too much interest, subtract amount that overpays from
// interest paid for period.
adjusted = interestForThisPeriod.minus(totalInterestRemaining.abs());
} else if (isLastRepaymentPeriod(this.actualNumberOfRepayments, periodNumber)) {
final Money interestDifference = totalCumulativeInterestToDate.minus(totalInterestDueForLoan);
if (interestDifference.isLessThanZero()) {
adjusted = interestForThisPeriod.plus(interestDifference.abs());
} else if (interestDifference.isGreaterThanZero()) {
adjusted = interestForThisPeriod.minus(interestDifference.abs());
}
}
if (adjusted.isLessThanZero()) {
adjusted = adjusted.plus(adjusted);
}
return adjusted;
}
/**
* Calculates the total interest to be charged on loan taking into account grace settings.
*
*/
public Money calculateTotalInterestCharged(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) {
Money totalInterestCharged = this.principal.zero();
switch (this.interestMethod) {
case FLAT:
final Money totalInterestChargedForLoanTerm = calculateTotalFlatInterestDueWithoutGrace(calculator, mc);
final Money totalInterestPerInstallment = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc);
final Money totalGraceOnInterestCharged = totalInterestPerInstallment.multiplyRetainScale(getInterestChargingGrace(),
mc.getRoundingMode());
totalInterestCharged = totalInterestChargedForLoanTerm.minus(totalGraceOnInterestCharged);
break;
case DECLINING_BALANCE:
case INVALID:
break;
}
return totalInterestCharged;
}
public Money calculateTotalPrincipalForPeriod(final PaymentPeriodsInOneYearCalculator calculator, final Money outstandingBalance,
final int periodNumber, final MathContext mc, Money interestForThisInstallment) {
Money principalForInstallment = this.principal.zero();
switch (this.interestMethod) {
case FLAT:
principalForInstallment = calculateTotalPrincipalPerPeriodWithoutGrace(mc, periodNumber, interestForThisInstallment);
break;
case DECLINING_BALANCE:
switch (this.amortizationMethod) {
case EQUAL_INSTALLMENTS:
Money totalPmtForThisInstallment = pmtForInstallment(calculator, outstandingBalance, periodNumber, mc);
principalForInstallment = calculatePrincipalDueForInstallment(periodNumber, totalPmtForThisInstallment,
interestForThisInstallment);
break;
case EQUAL_PRINCIPAL:
principalForInstallment = calculateEqualPrincipalDueForInstallment(mc, periodNumber);
break;
case INVALID:
break;
}
break;
case INVALID:
break;
}
return principalForInstallment;
}
public Money pmtForInstallment(final PaymentPeriodsInOneYearCalculator calculator, final Money outstandingBalance,
final int periodNumber, final MathContext mc) {
// Calculate exact period from disbursement date
final LocalDate periodStartDate = getExpectedDisbursementDate().withDayOfMonth(1);
final LocalDate periodEndDate = getPeriodEndDate(periodStartDate);
// equal installments
final int periodsElapsed = periodNumber - 1;
// with periodic interest for default month and year for
// equal installment
final BigDecimal periodicInterestRateForRepaymentPeriod = periodicInterestRate(calculator, mc, DaysInMonthType.DAYS_30,
DaysInYearType.DAYS_365, periodStartDate, periodEndDate, true);
Money totalPmtForThisInstallment = calculateTotalDueForEqualInstallmentRepaymentPeriod(periodicInterestRateForRepaymentPeriod,
outstandingBalance, periodsElapsed);
return totalPmtForThisInstallment;
}
private LocalDate getPeriodEndDate(final LocalDate startDate) {
LocalDate dueRepaymentPeriodDate = startDate;
switch (this.repaymentPeriodFrequencyType) {
case DAYS:
dueRepaymentPeriodDate = startDate.plusDays(this.repaymentEvery);
break;
case WEEKS:
dueRepaymentPeriodDate = startDate.plusWeeks(this.repaymentEvery);
break;
case MONTHS:
dueRepaymentPeriodDate = startDate.plusMonths(this.repaymentEvery);
break;
case YEARS:
dueRepaymentPeriodDate = startDate.plusYears(this.repaymentEvery);
break;
case INVALID:
break;
case WHOLE_TERM:
log.error("TODO Implement getPeriodEndDate for WHOLE_TERM");
break;
}
return dueRepaymentPeriodDate;
}
public PrincipalInterest calculateTotalInterestForPeriod(final PaymentPeriodsInOneYearCalculator calculator,
final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, final int periodNumber, final MathContext mc,
final Money cumulatingInterestPaymentDueToGrace, final Money outstandingBalance, final LocalDate periodStartDate,
final LocalDate periodEndDate) {
Money interestForInstallment = this.principal.zero();
Money interestBroughtForwardDueToGrace = cumulatingInterestPaymentDueToGrace.copy();
InterestMethod interestMethod = this.interestMethod;
if (this.isEqualAmortization() && this.totalInterestDue != null) {
interestMethod = InterestMethod.FLAT;
}
switch (interestMethod) {
case FLAT:
if (this.isEqualAmortization() && this.totalInterestDue != null && this.interestMethod.isDecliningBalance()) {
interestForInstallment = flatInterestPerInstallment(mc, this.totalInterestDue);
} else {
switch (this.amortizationMethod) {
case EQUAL_INSTALLMENTS:
// average out outstanding interest over remaining
// instalments where interest is applicable
interestForInstallment = calculateTotalFlatInterestForInstallmentAveragingOutGracePeriods(calculator,
periodNumber, mc);
break;
case EQUAL_PRINCIPAL:
// interest follows time-value of money and is
// brought
// forward to next applicable interest payment
// period
final PrincipalInterest result = calculateTotalFlatInterestForPeriod(calculator, periodNumber, mc,
interestBroughtForwardDueToGrace);
interestForInstallment = result.interest();
interestBroughtForwardDueToGrace = result.interestPaymentDueToGrace();
break;
case INVALID:
break;
}
}
break;
case DECLINING_BALANCE:
final Money interestForThisInstallmentBeforeGrace = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(
calculator, mc, outstandingBalance, periodStartDate, periodEndDate);
final Money interestForThisInstallmentAfterGrace = calculateDecliningInterestDueForInstallmentAfterApplyingGrace(calculator,
interestCalculationGraceOnRepaymentPeriodFraction, mc, outstandingBalance, periodNumber, periodStartDate,
periodEndDate);
interestForInstallment = interestForThisInstallmentAfterGrace;
if (interestForThisInstallmentAfterGrace.isGreaterThanZero()) {
interestForInstallment = interestBroughtForwardDueToGrace.plus(interestForThisInstallmentAfterGrace);
interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.zero();
} else if (isInterestFreeGracePeriod(periodNumber)) {
interestForInstallment = interestForInstallment.zero();
} else {
interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.plus(interestForThisInstallmentBeforeGrace);
}
break;
case INVALID:
break;
}
return new PrincipalInterest(null, interestForInstallment, interestBroughtForwardDueToGrace);
}
private boolean isLastRepaymentPeriod(final int numberOfRepayments, final int periodNumber) {
return periodNumber == numberOfRepayments;
}
public boolean isLastRepaymentPeriod(final int periodNumber) {
return periodNumber == this.actualNumberOfRepayments;
}
/**
* general method to calculate totalInterestDue discounting any grace settings
*/
private Money calculateTotalFlatInterestDueWithoutGrace(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) {
Money totalInterestDue = this.principal.zero();
switch (this.interestMethod) {
case FLAT:
final BigDecimal interestRateForLoanTerm = calculateFlatInterestRateForLoanTerm(calculator, mc);
totalInterestDue = this.disbursedPrincipal.minus(totalPrincipalAccountedForInterestCalcualtion)
.multiplyRetainScale(interestRateForLoanTerm, mc.getRoundingMode());
break;
case DECLINING_BALANCE:
break;
case INVALID:
break;
}
return totalInterestDue;
}
private BigDecimal calculateFlatInterestRateForLoanTerm(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) {
final BigDecimal divisor = BigDecimal.valueOf(Double.parseDouble("100.0"));
final long loanTermPeriodsInOneYear = calculatePeriodsInOneYear(calculator);
final BigDecimal loanTermPeriodsInYearBigDecimal = BigDecimal.valueOf(loanTermPeriodsInOneYear);
final BigDecimal loanTermFrequencyBigDecimal = calculatePeriodsInLoanTerm();
return this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc).divide(divisor, mc)
.multiply(loanTermFrequencyBigDecimal);
}
private BigDecimal calculatePeriodsInLoanTerm() {
BigDecimal periodsInLoanTerm = BigDecimal.valueOf(this.loanTermFrequency);
switch (this.interestCalculationPeriodMethod) {
case DAILY:
// number of days from 'ideal disbursement' to final date
LocalDate loanStartDate = getExpectedDisbursementDate();
if (DateUtils.isBefore(loanStartDate, getInterestChargedFromLocalDate())) {
loanStartDate = getInterestChargedFromLocalDate();
}
final int periodsInLoanTermInteger = Math.toIntExact(ChronoUnit.DAYS.between(loanStartDate, this.loanEndDate));
periodsInLoanTerm = BigDecimal.valueOf(periodsInLoanTermInteger);
break;
case INVALID:
break;
case SAME_AS_REPAYMENT_PERIOD:
if (this.allowPartialPeriodInterestCalcualtion) {
LocalDate startDate = getExpectedDisbursementDate();
if (getInterestChargedFromDate() != null) {
startDate = getInterestChargedFromLocalDate();
}
periodsInLoanTerm = calculatePeriodsBetweenDates(startDate, this.loanEndDate);
}
break;
}
return periodsInLoanTerm;
}
public BigDecimal calculatePeriodsBetweenDates(final LocalDate startDate, final LocalDate endDate) {
BigDecimal numberOfPeriods = BigDecimal.ZERO;
switch (this.repaymentPeriodFrequencyType) {
case DAYS:
int numOfDays = Math.toIntExact(ChronoUnit.DAYS.between(startDate, endDate));
numberOfPeriods = BigDecimal.valueOf((double) numOfDays);
break;
case WEEKS:
int numberOfWeeks = Math.toIntExact(ChronoUnit.WEEKS.between(startDate, endDate));
int daysLeftAfterWeeks = Math.toIntExact(ChronoUnit.DAYS.between(startDate.plusWeeks(numberOfWeeks), endDate));
numberOfPeriods = numberOfPeriods.add(BigDecimal.valueOf(numberOfWeeks))
.add(BigDecimal.valueOf((double) daysLeftAfterWeeks / 7));
break;
case MONTHS:
int numberOfMonths = Math.toIntExact(ChronoUnit.MONTHS.between(startDate, endDate));
LocalDate startDateAfterConsideringMonths = null;
LocalDate endDateAfterConsideringMonths = null;
int diffDays = 0;
if (this.loanCalendar == null) {
startDateAfterConsideringMonths = startDate.plusMonths(numberOfMonths);
startDateAfterConsideringMonths = (LocalDate) CalendarUtils.adjustDate(startDateAfterConsideringMonths, getSeedDate(),
this.repaymentPeriodFrequencyType);
endDateAfterConsideringMonths = startDate.plusMonths(numberOfMonths + 1);
endDateAfterConsideringMonths = (LocalDate) CalendarUtils.adjustDate(endDateAfterConsideringMonths, getSeedDate(),
this.repaymentPeriodFrequencyType);
} else {
LocalDate expectedStartDate = startDate;
if (!CalendarUtils.isValidRedurringDate(loanCalendar.getRecurrence(),
loanCalendar.getStartDateLocalDate().minusMonths(getRepaymentEvery()), startDate)) {
expectedStartDate = CalendarUtils.getNewRepaymentMeetingDate(loanCalendar.getRecurrence(),
startDate.minusMonths(getRepaymentEvery()), startDate.minusMonths(getRepaymentEvery()), getRepaymentEvery(),
CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(getLoanTermPeriodFrequencyType()),
this.holidayDetailDTO.getWorkingDays(), isSkipRepaymentOnFirstDayOfMonth, numberOfDays);
}
if (!DateUtils.isEqual(expectedStartDate, startDate)) {
diffDays = Math.toIntExact(ChronoUnit.DAYS.between(startDate, expectedStartDate));
}
if (numberOfMonths == 0) {
startDateAfterConsideringMonths = expectedStartDate;
} else {
startDateAfterConsideringMonths = CalendarUtils.getNewRepaymentMeetingDate(loanCalendar.getRecurrence(),
expectedStartDate, expectedStartDate.plusMonths(numberOfMonths), getRepaymentEvery(),
CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(getLoanTermPeriodFrequencyType()),
this.holidayDetailDTO.getWorkingDays(), isSkipRepaymentOnFirstDayOfMonth, numberOfDays);
}
endDateAfterConsideringMonths = CalendarUtils.getNewRepaymentMeetingDate(loanCalendar.getRecurrence(),
startDateAfterConsideringMonths, startDateAfterConsideringMonths.plusDays(1), getRepaymentEvery(),
CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(getLoanTermPeriodFrequencyType()),
this.holidayDetailDTO.getWorkingDays(), isSkipRepaymentOnFirstDayOfMonth, numberOfDays);
}
int daysLeftAfterMonths = Math.toIntExact(ChronoUnit.DAYS.between(startDateAfterConsideringMonths, endDate)) + diffDays;
int daysInPeriodAfterMonths = Math
.toIntExact(ChronoUnit.DAYS.between(startDateAfterConsideringMonths, endDateAfterConsideringMonths));
numberOfPeriods = numberOfPeriods.add(BigDecimal.valueOf(numberOfMonths))
.add(BigDecimal.valueOf((double) daysLeftAfterMonths / daysInPeriodAfterMonths));
break;
case YEARS:
int numberOfYears = Math.toIntExact(ChronoUnit.YEARS.between(startDate, endDate));
LocalDate startDateAfterConsideringYears = startDate.plusYears(numberOfYears);
LocalDate endDateAfterConsideringYears = startDate.plusYears(numberOfYears + 1);
int daysLeftAfterYears = Math.toIntExact(ChronoUnit.DAYS.between(startDateAfterConsideringYears, endDate));
int daysInPeriodAfterYears = Math
.toIntExact(ChronoUnit.DAYS.between(startDateAfterConsideringYears, endDateAfterConsideringYears));
numberOfPeriods = numberOfPeriods.add(BigDecimal.valueOf(numberOfYears))
.add(BigDecimal.valueOf((double) daysLeftAfterYears / daysInPeriodAfterYears));
break;
default:
break;
}
return numberOfPeriods;
}
public void updateLoanEndDate(final LocalDate loanEndDate) {
this.loanEndDate = loanEndDate;
}
private Money calculateTotalInterestPerInstallmentWithoutGrace(final PaymentPeriodsInOneYearCalculator calculator,
final MathContext mc) {
final Money totalInterestForLoanTerm = calculateTotalFlatInterestDueWithoutGrace(calculator, mc);
return flatInterestPerInstallment(mc, totalInterestForLoanTerm);
}
private Money flatInterestPerInstallment(final MathContext mc, final Money totalInterestForLoanTerm) {
Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(
Long.valueOf(this.actualNumberOfRepayments) - defaultToZeroIfNull(this.excludePeriodsForCalculation), mc.getRoundingMode());
if (this.excludePeriodsForCalculation < this.periodsCompleted) {
Money interestLeft = this.totalInterestDue.minus(this.totalInterestAccounted);
interestPerInstallment = interestLeft.dividedBy(
Long.valueOf(this.actualNumberOfRepayments) - defaultToZeroIfNull(this.periodsCompleted), mc.getRoundingMode());
}
return interestPerInstallment;
}
private Money calculateTotalPrincipalPerPeriodWithoutGrace(final MathContext mc, final int periodNumber,
Money interestForThisInstallment) {
final int totalRepaymentsWithCapitalPayment = calculateNumberOfRepaymentsWithPrincipalPayment();
Money principalPerPeriod = null;
if (getFixedEmiAmount() == null) {
if (this.fixedPrincipalPercentagePerInstallment != null) {
principalPerPeriod = this.principal.minus(totalPrincipalAccounted)
.percentageOf(this.fixedPrincipalPercentagePerInstallment, mc.getRoundingMode())
.plus(this.adjustPrincipalForFlatLoans);
} else {
principalPerPeriod = this.principal.minus(totalPrincipalAccounted)
.dividedBy(totalRepaymentsWithCapitalPayment, mc.getRoundingMode()).plus(this.adjustPrincipalForFlatLoans);
}
if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) {
principalPerPeriod = principalPerPeriod.zero();
}
if (!isPrincipalGraceApplicableForThisPeriod(periodNumber) && currentPeriodFixedPrincipalAmount != null) {
this.adjustPrincipalForFlatLoans = this.adjustPrincipalForFlatLoans
.plus(principalPerPeriod.minus(currentPeriodFixedPrincipalAmount)
.dividedBy(this.actualNumberOfRepayments - periodNumber, mc.getRoundingMode()));
principalPerPeriod = this.principal.zero().plus(currentPeriodFixedPrincipalAmount);
}
if (this.installmentAmountInMultiplesOf != null) {
Money roundedPrincipalPerPeriod = Money.roundToMultiplesOf(principalPerPeriod, this.installmentAmountInMultiplesOf);
if (interestForThisInstallment != null) {
Money roundedInterestForThisInstallment = Money.roundToMultiplesOf(interestForThisInstallment,
this.installmentAmountInMultiplesOf);
/*
* Thinking is
*
* principalPerPeriod 416.67 -> 417 interestForThisInstallment 12.50 -> 13
*
* Sum: 417 + 13 - 12.5 = 417.5 as principal so the total outstanding amount is in line with the
* installmentAmountInMultiplesOf setting
*/
principalPerPeriod = roundedPrincipalPerPeriod.add(roundedInterestForThisInstallment).minus(interestForThisInstallment);
} else {
principalPerPeriod = roundedPrincipalPerPeriod;
}
}
} else {
principalPerPeriod = Money.of(this.getCurrency(), getFixedEmiAmount()).minus(interestForThisInstallment);
return principalPerPeriod;
}
return principalPerPeriod;
}
private PrincipalInterest calculateTotalFlatInterestForPeriod(final PaymentPeriodsInOneYearCalculator calculator,
final int periodNumber, final MathContext mc, final Money cumulatingInterestPaymentDueToGrace) {
Money interestBroughtForwardDueToGrace = cumulatingInterestPaymentDueToGrace.copy();
Money interestForInstallment = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc);
if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) {
interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.plus(interestForInstallment);
interestForInstallment = interestForInstallment.zero();
} else if (isInterestFreeGracePeriod(periodNumber)) {
interestForInstallment = interestForInstallment.zero();
} else if (isFirstPeriodAfterInterestPaymentGracePeriod(periodNumber)) {
interestForInstallment = cumulatingInterestPaymentDueToGrace.plus(interestForInstallment);
interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.zero();
}
return new PrincipalInterest(null, interestForInstallment, interestBroughtForwardDueToGrace);
}
/*
* calculates the interest that should be due for a given scheduled loan repayment period. It takes into account
* GRACE periods and calculates how much interest is due per period by averaging the number of periods where
* interest is due and should be paid against the total known interest that is due without grace.
*/
private Money calculateTotalFlatInterestForInstallmentAveragingOutGracePeriods(final PaymentPeriodsInOneYearCalculator calculator,
final int periodNumber, final MathContext mc) {
Money interestForInstallment = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc);
if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) {
interestForInstallment = interestForInstallment.zero();
} else if (isInterestFreeGracePeriod(periodNumber)) {
interestForInstallment = interestForInstallment.zero();
} else {
final Money totalInterestForLoanTerm = calculateTotalFlatInterestDueWithoutGrace(calculator, mc);
final Money interestPerGracePeriod = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc);
final Money totalInterestFree = interestPerGracePeriod.multipliedBy(getInterestChargingGrace());
final Money realTotalInterestForLoan = totalInterestForLoanTerm.minus(totalInterestFree);
Integer interestPaymentDuePeriods = calculateNumberOfRemainingInterestPaymentPeriods(this.actualNumberOfRepayments,
this.excludePeriodsForCalculation);
interestForInstallment = realTotalInterestForLoan.dividedBy(BigDecimal.valueOf(interestPaymentDuePeriods),
mc.getRoundingMode());
if (this.excludePeriodsForCalculation < this.periodsCompleted) {
Money interestLeft = this.totalInterestDue.minus(this.totalInterestAccounted);
Integer interestDuePeriods = calculateNumberOfRemainingInterestPaymentPeriods(this.actualNumberOfRepayments,
this.periodsCompleted);
interestForInstallment = interestLeft.dividedBy(Long.valueOf(interestDuePeriods), mc.getRoundingMode());
}
if (!this.periodNumbersApplicableForInterestGrace.isEmpty()) {
int periodsElapsed = calculateLastInterestGracePeriod(periodNumber);
if (periodsElapsed > this.excludePeriodsForCalculation && periodsElapsed > this.periodsCompleted) {
Money interestLeft = this.totalInterestDue.minus(this.totalInterestAccounted);
Integer interestDuePeriods = calculateNumberOfRemainingInterestPaymentPeriods(this.actualNumberOfRepayments,
periodsElapsed);
interestForInstallment = interestLeft.dividedBy(Long.valueOf(interestDuePeriods), mc.getRoundingMode());
}
}
}
return interestForInstallment;
}
private BigDecimal periodicInterestRate(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc,
final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType, LocalDate periodStartDate,
LocalDate periodEndDate) {
return periodicInterestRate(calculator, mc, daysInMonthType, daysInYearType, periodStartDate, periodEndDate, false);
}
private BigDecimal periodicInterestRate(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc,
final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType, LocalDate periodStartDate, LocalDate periodEndDate,
boolean isForPMT) {
final long loanTermPeriodsInOneYear = calculatePeriodsInOneYear(calculator);
final BigDecimal divisor = BigDecimal.valueOf(Double.parseDouble("100.0"));
final BigDecimal loanTermPeriodsInYearBigDecimal = BigDecimal.valueOf(loanTermPeriodsInOneYear);
BigDecimal periodicInterestRate = BigDecimal.ZERO;
BigDecimal loanTermFrequencyBigDecimal = BigDecimal.ONE;
if (isForPMT) {
loanTermFrequencyBigDecimal = BigDecimal.valueOf(this.repaymentEvery);
} else {
loanTermFrequencyBigDecimal = calculateLoanTermFrequency(periodStartDate, periodEndDate);
}
switch (this.interestCalculationPeriodMethod) {
case INVALID:
break;
case DAILY:
// For daily work out number of days in the period
BigDecimal numberOfDaysInPeriod = BigDecimal.valueOf(ChronoUnit.DAYS.between(periodStartDate, periodEndDate));
final BigDecimal oneDayOfYearInterestRate = this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc)
.divide(divisor, mc);
switch (this.repaymentPeriodFrequencyType) {
case INVALID:
break;
case DAYS:
periodicInterestRate = oneDayOfYearInterestRate.multiply(numberOfDaysInPeriod, mc);
break;
case WEEKS:
periodicInterestRate = oneDayOfYearInterestRate.multiply(numberOfDaysInPeriod, mc);
break;
case MONTHS:
if (daysInMonthType.isDaysInMonth_30()) {
numberOfDaysInPeriod = loanTermFrequencyBigDecimal.multiply(BigDecimal.valueOf(30), mc);
}
periodicInterestRate = oneDayOfYearInterestRate.multiply(numberOfDaysInPeriod, mc);
break;
case YEARS:
switch (daysInYearType) {
case DAYS_360:
numberOfDaysInPeriod = loanTermFrequencyBigDecimal.multiply(BigDecimal.valueOf(360), mc);
break;
case DAYS_364:
numberOfDaysInPeriod = loanTermFrequencyBigDecimal.multiply(BigDecimal.valueOf(364), mc);
break;
case DAYS_365:
numberOfDaysInPeriod = loanTermFrequencyBigDecimal.multiply(BigDecimal.valueOf(365), mc);
break;
default:
break;
}
periodicInterestRate = oneDayOfYearInterestRate.multiply(numberOfDaysInPeriod, mc);
break;
case WHOLE_TERM:
log.error("TODO Implement periodicInterestRate for WHOLE_TERM");
break;
}
break;
case SAME_AS_REPAYMENT_PERIOD:
periodicInterestRate = this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc).divide(divisor, mc)
.multiply(loanTermFrequencyBigDecimal);
break;
}
return periodicInterestRate;
}
private BigDecimal calculateLoanTermFrequency(final LocalDate periodStartDate, final LocalDate periodEndDate) {
BigDecimal loanTermFrequencyBigDecimal = BigDecimal.valueOf(this.repaymentEvery);
if (this.interestCalculationPeriodMethod.isDaily() || this.allowPartialPeriodInterestCalcualtion) {
loanTermFrequencyBigDecimal = calculatePeriodsBetweenDates(periodStartDate, periodEndDate);
}
return loanTermFrequencyBigDecimal;
}
public BigDecimal interestRateFor(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc,
final Money outstandingBalance, final LocalDate fromDate, final LocalDate toDate) {
long loanTermPeriodsInOneYear = calculator.calculate(PeriodFrequencyType.DAYS).longValue();
int repaymentEvery = Math.toIntExact(ChronoUnit.DAYS.between(fromDate, toDate));
if (isFallingInRepaymentPeriod(fromDate, toDate)) {
loanTermPeriodsInOneYear = calculatePeriodsInOneYear(calculator);
repaymentEvery = getPeriodsBetween(fromDate, toDate);
}
final BigDecimal divisor = BigDecimal.valueOf(Double.parseDouble("100.0"));
final BigDecimal loanTermPeriodsInYearBigDecimal = BigDecimal.valueOf(loanTermPeriodsInOneYear);
final BigDecimal oneDayOfYearInterestRate = this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc)
.divide(divisor, mc);
BigDecimal interestRate = oneDayOfYearInterestRate.multiply(BigDecimal.valueOf(repaymentEvery), mc);
return outstandingBalance.getAmount().multiply(interestRate, mc);
}
private long calculatePeriodsInOneYear(final PaymentPeriodsInOneYearCalculator calculator) {
// check if daysInYears is set if so change periodsInOneYear to days set
// in db
long periodsInOneYear;
boolean daysInYearToUse = (this.repaymentPeriodFrequencyType.getCode().equalsIgnoreCase("periodFrequencyType.days")
&& !this.daysInYearType.getCode().equalsIgnoreCase("DaysInYearType.actual"));
if (daysInYearToUse) {
periodsInOneYear = this.daysInYearType.getValue().longValue();
} else {
periodsInOneYear = calculator.calculate(this.repaymentPeriodFrequencyType).longValue();
}
switch (this.interestCalculationPeriodMethod) {
case DAILY:
periodsInOneYear = !this.daysInYearType.getCode().equalsIgnoreCase("DaysInYearType.actual")
? this.daysInYearType.getValue().longValue()
: calculator.calculate(PeriodFrequencyType.DAYS).longValue();
break;
case INVALID:
break;
case SAME_AS_REPAYMENT_PERIOD:
break;
}
return periodsInOneYear;
}
private int calculateNumberOfRepaymentsWithPrincipalPayment() {
int numPeriods = calculateNumberOfRemainingPrincipalPaymentPeriods(this.actualNumberOfRepayments, this.periodsCompleted);
// numPeriods = numPeriods - this.periodsCompleted;
return numPeriods;
}
private Integer calculateNumberOfRemainingInterestPaymentPeriods(final Integer totalNumberOfRepaymentPeriods, int periodsElapsed) {
int principalFeePeriods = 0;
for (Integer intNumber : this.periodNumbersApplicableForInterestGrace) {
if (intNumber > periodsElapsed) {
principalFeePeriods++;
}
}
Integer periodsRemaining = totalNumberOfRepaymentPeriods - periodsElapsed - principalFeePeriods;
/**
* if grace period available then need to be subtracted
*/
if (this.interestChargingGrace != null) {
periodsRemaining = periodsRemaining - this.interestChargingGrace;
}
return periodsRemaining;
}
private Integer calculateLastInterestGracePeriod(int periodNumber) {
int lastGracePeriod = 0;
for (Integer grace : this.periodNumbersApplicableForInterestGrace) {
if (grace < periodNumber && lastGracePeriod < grace) {
lastGracePeriod = grace;
}
}
return lastGracePeriod;
}
public boolean isPrincipalGraceApplicableForThisPeriod(final int periodNumber) {
boolean isPrincipalGraceApplicableForThisPeriod = false;
if (this.periodNumbersApplicableForPrincipalGrace.contains(periodNumber)) {
isPrincipalGraceApplicableForThisPeriod = true;
}
return isPrincipalGraceApplicableForThisPeriod;
}
public boolean isInterestPaymentGraceApplicableForThisPeriod(final int periodNumber) {
boolean isInterestPaymentGraceApplicableForThisPeriod = false;
if (this.periodNumbersApplicableForInterestGrace.contains(periodNumber)) {
isInterestPaymentGraceApplicableForThisPeriod = true;
}
return isInterestPaymentGraceApplicableForThisPeriod;
}
private boolean isFirstPeriodAfterInterestPaymentGracePeriod(final int periodNumber) {
return periodNumber > 0 && periodNumber == getInterestPaymentGrace() + 1;
}
private boolean isInterestFreeGracePeriod(final int periodNumber) {
return periodNumber > 0 && periodNumber <= getInterestChargingGrace();
}
public Integer getPrincipalGrace() {
Integer graceOnPrincipalPayments = Integer.valueOf(0);
if (this.principalGrace != null) {
graceOnPrincipalPayments = this.principalGrace;
}
return graceOnPrincipalPayments;
}
public Integer getRecurringMoratoriumOnPrincipalPeriods() {
Integer recurringMoratoriumOnPrincipalPeriods = Integer.valueOf(0);
if (this.recurringMoratoriumOnPrincipalPeriods != null) {
recurringMoratoriumOnPrincipalPeriods = this.recurringMoratoriumOnPrincipalPeriods;
}
return recurringMoratoriumOnPrincipalPeriods;
}
public Integer getInterestPaymentGrace() {
Integer graceOnInterestPayments = Integer.valueOf(0);
if (this.interestPaymentGrace != null) {
graceOnInterestPayments = this.interestPaymentGrace;
}
return graceOnInterestPayments;
}
public Integer getInterestChargingGrace() {
Integer graceOnInterestCharged = Integer.valueOf(0);
if (this.interestChargingGrace != null) {
graceOnInterestCharged = this.interestChargingGrace;
}
return graceOnInterestCharged;
}
private double paymentPerPeriod(final BigDecimal periodicInterestRate, final Money balance, final int periodsElapsed) {
if (getFixedEmiAmount() == null) {
final double futureValue = 0;
final double principalDouble = balance.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue();
final Integer periodsRemaining = calculateNumberOfRemainingPrincipalPaymentPeriods(this.actualNumberOfRepayments,
periodsElapsed);
double installmentAmount = FinanicalFunctions.pmt(periodicInterestRate.doubleValue(), periodsRemaining.doubleValue(),
principalDouble, futureValue, false);
if (this.installmentAmountInMultiplesOf != null) {
installmentAmount = Money.roundToMultiplesOf(installmentAmount, this.installmentAmountInMultiplesOf);
}
setFixedEmiAmount(BigDecimal.valueOf(installmentAmount));
}
return getFixedEmiAmount().doubleValue();
}
private Money calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator,
final MathContext mc, final Money outstandingBalance, LocalDate periodStartDate, LocalDate periodEndDate) {
Money interestDue = Money.zero(outstandingBalance.getCurrency());
final BigDecimal periodicInterestRate = periodicInterestRate(calculator, mc, this.daysInMonthType, this.daysInYearType,
periodStartDate, periodEndDate);// 0.021232877 ob:14911.64
interestDue = outstandingBalance.multiplyRetainScale(periodicInterestRate, mc.getRoundingMode());
return interestDue;
}
private Money calculateDecliningInterestDueForInstallmentAfterApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator,
final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, final MathContext mc, final Money outstandingBalance,
final int periodNumber, LocalDate periodStartDate, LocalDate periodEndDate) {
Money interest = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, mc, outstandingBalance, periodStartDate,
periodEndDate);
if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) {
interest = interest.zero();
}
BigDecimal fraction = interestCalculationGraceOnRepaymentPeriodFraction;
if (isInterestFreeGracePeriod(periodNumber)) {
interest = interest.zero();
} else if (isInterestFreeGracePeriodFromDate(interestCalculationGraceOnRepaymentPeriodFraction)) {
if (interestCalculationGraceOnRepaymentPeriodFraction.compareTo(BigDecimal.ZERO) > 0) {
interest = interest.zero();
fraction = fraction.subtract(BigDecimal.ONE);
} else if (interestCalculationGraceOnRepaymentPeriodFraction.compareTo(BigDecimal.valueOf(0.25)) > 0
&& interestCalculationGraceOnRepaymentPeriodFraction.compareTo(BigDecimal.ONE) < 0) {
final Money graceOnInterestForRepaymentPeriod = interest.multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction);
interest = interest.minus(graceOnInterestForRepaymentPeriod);
fraction = BigDecimal.ZERO;
}
}
return interest;
}
private boolean isInterestFreeGracePeriodFromDate(BigDecimal interestCalculationGraceOnRepaymentPeriodFraction) {
return this.interestChargedFromDate != null && interestCalculationGraceOnRepaymentPeriodFraction.compareTo(BigDecimal.ZERO) > 0;
}
private Money calculateEqualPrincipalDueForInstallment(final MathContext mc, final int periodNumber) {
Money principal = this.principal;
if (this.fixedPrincipalAmount == null) {
final Integer numberOfPrincipalPaymentPeriods = calculateNumberOfRemainingPrincipalPaymentPeriods(this.actualNumberOfRepayments,
periodNumber);
principal = this.principal.dividedBy(numberOfPrincipalPaymentPeriods, mc.getRoundingMode());
this.fixedPrincipalAmount = principal.getAmount();
}
if (this.fixedPrincipalPercentagePerInstallment != null) {
principal = this.principal.percentageOf(this.fixedPrincipalPercentagePerInstallment, mc.getRoundingMode());
} else {
principal = Money.of(getCurrency(), getFixedPrincipalAmount());
}
if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) {
principal = principal.zero();
}
return principal;
}
public void updateFixedPrincipalAmount(final MathContext mc, final int periodNumber, final Money outstandingAmount) {
final Integer numberOfPrincipalPaymentPeriods = calculateNumberOfRemainingPrincipalPaymentPeriods(this.actualNumberOfRepayments,
periodNumber - 1);
Money principal = outstandingAmount.dividedBy(numberOfPrincipalPaymentPeriods, mc.getRoundingMode());
this.fixedPrincipalAmount = principal.getAmount();
}
private Integer calculateNumberOfRemainingPrincipalPaymentPeriods(final Integer totalNumberOfRepaymentPeriods, int periodsElapsed) {
int principalFeePeriods = 0;
for (Integer intNumber : this.periodNumbersApplicableForPrincipalGrace) {
if (intNumber > periodsElapsed) {
principalFeePeriods++;
}
}
Integer periodsRemaining = totalNumberOfRepaymentPeriods - periodsElapsed - principalFeePeriods;
return periodsRemaining;
}
public void setFixedPrincipalAmount(BigDecimal fixedPrincipalAmount) {
this.fixedPrincipalAmount = fixedPrincipalAmount;
}
private Money calculatePrincipalDueForInstallment(final int periodNumber, final Money totalDuePerInstallment,
final Money periodInterest) {
Money principal = totalDuePerInstallment.minus(periodInterest);
if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) {
principal = principal.zero();
}
return principal;
}
private Money calculateTotalDueForEqualInstallmentRepaymentPeriod(final BigDecimal periodicInterestRate, final Money balance,
final int periodsElapsed) {
final double paymentPerRepaymentPeriod = paymentPerPeriod(periodicInterestRate, balance, periodsElapsed);
return Money.of(balance.getCurrency(), BigDecimal.valueOf(paymentPerRepaymentPeriod));
}
public LoanProductRelatedDetail toLoanProductRelatedDetail() {
final MonetaryCurrency currency = new MonetaryCurrency(this.currency.getCode(), this.currency.getDecimalPlaces(),
this.currency.getCurrencyInMultiplesOf());
return LoanProductRelatedDetail.createFrom(currency, this.principal.getAmount(), this.interestRatePerPeriod,
this.interestRatePeriodFrequencyType, this.annualNominalInterestRate, this.interestMethod,
this.interestCalculationPeriodMethod, this.allowPartialPeriodInterestCalcualtion, this.repaymentEvery,
this.repaymentPeriodFrequencyType, this.numberOfRepayments, this.principalGrace, this.recurringMoratoriumOnPrincipalPeriods,
this.interestPaymentGrace, this.interestChargingGrace, this.amortizationMethod, this.inArrearsTolerance.getAmount(),
this.graceOnArrearsAgeing, this.daysInMonthType.getValue(), this.daysInYearType.getValue(),
this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled,
this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled, this.loanScheduleType,
this.loanScheduleProcessingType, this.fixedLength);
}
public Integer getLoanTermFrequency() {
return this.loanTermFrequency;
}
public PeriodFrequencyType getLoanTermPeriodFrequencyType() {
return this.loanTermPeriodFrequencyType;
}
public Integer getRepaymentEvery() {
return this.repaymentEvery;
}
public PeriodFrequencyType getRepaymentPeriodFrequencyType() {
return this.repaymentPeriodFrequencyType;
}
public LocalDate getRepaymentStartFromDate() {
return this.repaymentsStartingFromDate;
}
public LocalDate getInterestChargedFromDate() {
return this.interestChargedFromDate;
}
public void setPrincipal(Money principal) {
this.principal = principal;
}
public void setDisbursedPrincipal(Money disbursedPrincipal) {
this.disbursedPrincipal = disbursedPrincipal;
}
public LocalDate getInterestChargedFromLocalDate() {
return this.interestChargedFromDate;
}
public InterestMethod getInterestMethod() {
return this.interestMethod;
}
public AmortizationMethod getAmortizationMethod() {
return this.amortizationMethod;
}
public MonetaryCurrency getCurrency() {
return this.principal.getCurrency();
}
public Integer getNumberOfRepayments() {
return this.numberOfRepayments;
}
public LocalDate getExpectedDisbursementDate() {
return this.expectedDisbursementDate;
}
public LocalDate getRepaymentsStartingFromLocalDate() {
return this.repaymentsStartingFromDate;
}
public LocalDate getCalculatedRepaymentsStartingFromLocalDate() {
return this.calculatedRepaymentsStartingFromDate;
}
public Money getPrincipal() {
return this.principal;
}
public Money getApprovedPrincipal() {
return this.approvedPrincipal;
}
public List<DisbursementData> getDisbursementDatas() {
return this.disbursementDatas;
}
public boolean isMultiDisburseLoan() {
return this.multiDisburseLoan;
}
@NotNull
public Money getMaxOutstandingBalance() {
return Money.of(getCurrency(), this.maxOutstandingBalance);
}
public BigDecimal getFixedEmiAmount() {
BigDecimal fixedEmiAmount = this.fixedEmiAmount;
if (getCurrentPeriodFixedEmiAmount() != null) {
fixedEmiAmount = getCurrentPeriodFixedEmiAmount();
}
return fixedEmiAmount;
}
public Integer getNthDay() {
return this.nthDay;
}
public DayOfWeekType getWeekDayType() {
return this.weekDayType;
}
public void setFixedEmiAmount(BigDecimal fixedEmiAmount) {
this.fixedEmiAmount = fixedEmiAmount;
}
public void resetFixedEmiAmount() {
this.fixedEmiAmount = this.actualFixedEmiAmount;
}
public LoanRescheduleStrategyMethod getLoanRescheduleStrategyMethod() {
return LoanRescheduleStrategyMethod.REDUCE_EMI_AMOUNT;
}
public boolean isInterestRecalculationEnabled() {
return this.interestRecalculationEnabled;
}
public LoanRescheduleStrategyMethod getRescheduleStrategyMethod() {
return this.rescheduleStrategyMethod;
}
public InterestRecalculationCompoundingMethod getInterestRecalculationCompoundingMethod() {
return this.interestRecalculationCompoundingMethod;
}
public CalendarInstance getRestCalendarInstance() {
return this.restCalendarInstance;
}
private boolean isFallingInRepaymentPeriod(LocalDate fromDate, LocalDate toDate) {
boolean isSameAsRepaymentPeriod = false;
if (this.interestCalculationPeriodMethod.getValue().equals(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD.getValue())) {
switch (this.repaymentPeriodFrequencyType) {
case WEEKS:
int days = Math.toIntExact(ChronoUnit.DAYS.between(fromDate, toDate));
isSameAsRepaymentPeriod = (days % 7) == 0;
break;
case MONTHS:
boolean isFromDateOnEndDate = false;
if (fromDate.getDayOfMonth() > fromDate.plusDays(1).getDayOfMonth()) {
isFromDateOnEndDate = true;
}
boolean isToDateOnEndDate = false;
if (toDate.getDayOfMonth() > toDate.plusDays(1).getDayOfMonth()) {
isToDateOnEndDate = true;
}
if (isFromDateOnEndDate && isToDateOnEndDate) {
isSameAsRepaymentPeriod = true;
} else {
int months = getPeriodsBetween(fromDate, toDate);
fromDate = fromDate.plusMonths(months);
isSameAsRepaymentPeriod = DateUtils.isEqual(fromDate, toDate);
}
break;
default:
break;
}
}
return isSameAsRepaymentPeriod;
}
private Integer getPeriodsBetween(LocalDate fromDate, LocalDate toDate) {
Integer numberOfPeriods = 0;
switch (this.repaymentPeriodFrequencyType) {
case DAYS:
numberOfPeriods = Math.toIntExact(ChronoUnit.DAYS.between(fromDate, toDate));
break;
case WEEKS:
numberOfPeriods = Math.toIntExact(ChronoUnit.WEEKS.between(fromDate, toDate));
break;
case MONTHS:
numberOfPeriods = Math.toIntExact(ChronoUnit.MONTHS.between(fromDate, toDate));
break;
case YEARS:
numberOfPeriods = Math.toIntExact(ChronoUnit.YEARS.between(fromDate, toDate));
break;
default:
break;
}
return numberOfPeriods;
}
public RecalculationFrequencyType getRecalculationFrequencyType() {
return this.recalculationFrequencyType;
}
public void updateNumberOfRepayments(final Integer numberOfRepayments) {
this.numberOfRepayments = numberOfRepayments;
this.actualNumberOfRepayments = numberOfRepayments + getLoanTermVariations().adjustNumberOfRepayments();
}
public void updatePrincipalGrace(final Integer principalGrace) {
this.principalGrace = principalGrace;
}
public void updateInterestPaymentGrace(final Integer interestPaymentGrace) {
this.interestPaymentGrace = interestPaymentGrace;
}
public void updateInterestRatePerPeriod(BigDecimal interestRatePerPeriod) {
if (interestRatePerPeriod != null) {
this.interestRatePerPeriod = interestRatePerPeriod;
}
}
public void updateAnnualNominalInterestRate(BigDecimal annualNominalInterestRate) {
if (annualNominalInterestRate != null) {
this.annualNominalInterestRate = annualNominalInterestRate;
}
}
public BigDecimal getAnnualNominalInterestRate() {
return this.annualNominalInterestRate;
}
public void updateInterestChargedFromDate(LocalDate interestChargedFromDate) {
if (interestChargedFromDate != null) {
this.interestChargedFromDate = interestChargedFromDate;
}
}
public void updateLoanTermFrequency(Integer loanTermFrequency) {
if (loanTermFrequency != null) {
this.loanTermFrequency = loanTermFrequency;
}
}
public void updateTotalInterestDue(Money totalInterestDue) {
this.totalInterestDue = totalInterestDue;
}
public ApplicationCurrency getApplicationCurrency() {
return this.currency;
}
public InterestCalculationPeriodMethod getInterestCalculationPeriodMethod() {
return this.interestCalculationPeriodMethod;
}
public LoanPreClosureInterestCalculationStrategy getPreClosureInterestCalculationStrategy() {
return this.preClosureInterestCalculationStrategy;
}
public CalendarInstance getCompoundingCalendarInstance() {
return this.compoundingCalendarInstance;
}
public RecalculationFrequencyType getCompoundingFrequencyType() {
return this.compoundingFrequencyType;
}
public BigDecimal getActualFixedEmiAmount() {
return this.actualFixedEmiAmount;
}
public Calendar getLoanCalendar() {
return loanCalendar;
}
public BigDecimal getFixedPrincipalAmount() {
BigDecimal fixedPrincipalAmount = this.fixedPrincipalAmount;
if (getCurrentPeriodFixedPrincipalAmount() != null) {
fixedPrincipalAmount = getCurrentPeriodFixedPrincipalAmount();
}
return fixedPrincipalAmount;
}
public LoanTermVariationsDataWrapper getLoanTermVariations() {
return this.variationsDataWrapper;
}
public BigDecimal getCurrentPeriodFixedEmiAmount() {
return this.currentPeriodFixedEmiAmount;
}
public void setCurrentPeriodFixedEmiAmount(BigDecimal currentPeriodFixedEmiAmount) {
this.currentPeriodFixedEmiAmount = currentPeriodFixedEmiAmount;
}
public BigDecimal getCurrentPeriodFixedPrincipalAmount() {
return this.currentPeriodFixedPrincipalAmount;
}
public void setCurrentPeriodFixedPrincipalAmount(BigDecimal currentPeriodFixedPrincipalAmount) {
this.currentPeriodFixedPrincipalAmount = currentPeriodFixedPrincipalAmount;
}
public Integer fetchNumberOfRepaymentsAfterExceptions() {
return this.actualNumberOfRepayments;
}
public LocalDate getSeedDate() {
return this.seedDate;
}
public CalendarHistoryDataWrapper getCalendarHistoryDataWrapper() {
return this.calendarHistoryDataWrapper;
}
public Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled() {
return this.isInterestChargedFromDateSameAsDisbursalDateEnabled;
}
public Integer getNumberOfdays() {
return numberOfDays;
}
public boolean isSkipRepaymentOnFirstDayofMonth() {
return isSkipRepaymentOnFirstDayOfMonth;
}
public HolidayDetailDTO getHolidayDetailDTO() {
return this.holidayDetailDTO;
}
public boolean allowCompoundingOnEod() {
return this.allowCompoundingOnEod;
}
public Money getTotalDisbursedAmount() {
Money disbursedAmount = Money.zero(getCurrency());
if (isMultiDisburseLoan()) {
for (DisbursementData disbursement : getDisbursementDatas()) {
if (disbursement.isDisbursed()) {
disbursedAmount = disbursedAmount.plus(disbursement.getPrincipal());
}
}
} else {
disbursedAmount = getPrincipal();
}
return disbursedAmount;
}
public Money getTotalMultiDisbursedAmount() {
Money disbursedAmount = Money.zero(getCurrency());
if (isMultiDisburseLoan()) {
for (DisbursementData disbursement : getDisbursementDatas()) {
disbursedAmount = disbursedAmount.plus(disbursement.getPrincipal());
}
} else {
disbursedAmount = getPrincipal();
}
return disbursedAmount;
}
public void updatePeriodNumberApplicableForPrincipalOrInterestGrace(final Integer periodsApplicationForGrace) {
int applicablePeriodNumber = periodsApplicationForGrace;
int graceOnPrincipal = defaultToZeroIfNull(this.principalGrace);
int graceOnInterest = defaultToZeroIfNull(this.interestPaymentGrace);
while (graceOnPrincipal > 0 || graceOnInterest > 0) {
if (graceOnPrincipal > 0) {
this.periodNumbersApplicableForPrincipalGrace.add(applicablePeriodNumber);
}
if (graceOnInterest > 0) {
this.periodNumbersApplicableForInterestGrace.add(applicablePeriodNumber);
}
applicablePeriodNumber++;
graceOnPrincipal--;
graceOnInterest--;
}
}
/**
* set the value to zero if the provided value is null
*
* @return integer value equal/greater than 0
**/
private Integer defaultToZeroIfNull(Integer value) {
return (value != null) ? value : 0;
}
public void updateExcludePeriodsForCalculation(Integer excludePeriodsForCalculation) {
this.excludePeriodsForCalculation = excludePeriodsForCalculation;
this.extraPeriods = 0;
}
public Integer getActualNoOfRepaymnets() {
return this.actualNumberOfRepayments;
}
public Money getTotalInterestDue() {
return this.totalInterestDue;
}
private void updateRecurringMoratoriumOnPrincipalPeriods(Integer periodNumber) {
Boolean isPrincipalGraceApplicableForThisPeriod = false;
Integer numberOfRepayments = this.actualNumberOfRepayments;
if (this.getRecurringMoratoriumOnPrincipalPeriods() > 0) {
while (numberOfRepayments > 0) {
isPrincipalGraceApplicableForThisPeriod = ((periodNumber > 0 && periodNumber <= getPrincipalGrace()) || (periodNumber > 0
&& (((periodNumber - getPrincipalGrace()) % (this.getRecurringMoratoriumOnPrincipalPeriods() + 1)) != 1)));
if (isPrincipalGraceApplicableForThisPeriod) {
this.periodNumbersApplicableForPrincipalGrace.add(periodNumber);
}
numberOfRepayments--;
periodNumber++;
}
}
}
public void setTotalPrincipalAccountedForInterestCalculation(Money totalPrincipalAccounted) {
this.totalPrincipalAccountedForInterestCalcualtion = totalPrincipalAccounted;
}
// Used for FLAT loans to calculate principal and interest per installment
public void updateAccountedTillPeriod(int periodNumber, Money totalPrincipalAccounted, Money totalInterestAccounted,
int extendPeriods) {
this.periodsCompleted = periodNumber;
this.totalPrincipalAccounted = totalPrincipalAccounted;
this.totalInterestAccounted = totalInterestAccounted;
this.extraPeriods = this.extraPeriods + extendPeriods;
}
public void updateTotalInterestAccounted(Money totalInterestAccounted) {
this.totalInterestAccounted = totalInterestAccounted;
}
public void setSeedDate(LocalDate seedDate) {
this.seedDate = seedDate;
}
public boolean isEqualAmortization() {
return isEqualAmortization;
}
public void setEqualAmortization(boolean isEqualAmortization) {
this.isEqualAmortization = isEqualAmortization;
}
public boolean isFirstRepaymentDateAllowedOnHoliday() {
return isFirstRepaymentDateAllowedOnHoliday;
}
public Money getInterestTobeApproppriated() {
return interestTobeApproppriated == null ? this.principal.zero() : interestTobeApproppriated;
}
public void setInterestTobeApproppriated(Money interestTobeApproppriated) {
this.interestTobeApproppriated = interestTobeApproppriated;
}
public Boolean isInterestTobeApproppriated() {
return interestTobeApproppriated != null && interestTobeApproppriated.isGreaterThanZero();
}
public boolean isInterestToBeRecoveredFirstWhenGreaterThanEMIEnabled() {
return isInterestToBeRecoveredFirstWhenGreaterThanEMI;
}
public boolean isPrincipalCompoundingDisabledForOverdueLoans() {
return isPrincipalCompoundingDisabledForOverdueLoans;
}
public LocalDate getNewScheduledDueDateStart() {
return newScheduledDueDateStart;
}
public void setNewScheduledDueDateStart(LocalDate newScheduledDueDateStart) {
this.newScheduledDueDateStart = newScheduledDueDateStart;
}
public boolean isDownPaymentEnabled() {
return isDownPaymentEnabled;
}
public BigDecimal getDisbursedAmountPercentageForDownPayment() {
return disbursedAmountPercentageForDownPayment;
}
public RepaymentStartDateType getRepaymentStartDateType() {
return repaymentStartDateType;
}
public LocalDate getSubmittedOnDate() {
return submittedOnDate;
}
public boolean isScheduleExtensionForDownPaymentDisabled() {
return isScheduleExtensionForDownPaymentDisabled;
}
public Integer getInstallmentAmountInMultiplesOf() {
return installmentAmountInMultiplesOf;
}
public LoanScheduleType getLoanScheduleType() {
return loanScheduleType;
}
public Money getDownPaymentAmount() {
return downPaymentAmount;
}
}