in fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java [2184:2533]
private LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, Loan loan,
final HolidayDetailDTO holidayDetailDTO,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, LocalDate rescheduleFrom,
final LocalDate scheduleTillDate) {
// Loan transactions to process and find the variation on payments
Collection<RecalculationDetail> recalculationDetails = new ArrayList<>();
List<LoanTransaction> transactions = loan.getLoanTransactions();
for (LoanTransaction loanTransaction : transactions) {
if (loanTransaction.isPaymentTransaction()) {
recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(),
LoanTransaction.copyTransactionProperties(loanTransaction)));
}
}
final boolean applyInterestRecalculation = loanApplicationTerms.isInterestBearingAndInterestRecalculationEnabled();
// for complete schedule generation
LoanScheduleParams loanScheduleParams = LoanScheduleParams.createLoanScheduleParamsForCompleteUpdate(recalculationDetails,
loanRepaymentScheduleTransactionProcessor, scheduleTillDate, applyInterestRecalculation, mc);
List<LoanScheduleModelPeriod> periods = new ArrayList<>();
final List<LoanRepaymentScheduleInstallment> retainedInstallments = new ArrayList<>();
// this block is to retain the schedule installments prior to the
// provided date and creates late and early payment details for further
// calculations
if (rescheduleFrom != null) {
Money principalToBeScheduled = getPrincipalToBeScheduled(loanApplicationTerms);
// actual outstanding balance for interest calculation
Money outstandingBalance = principalToBeScheduled;
loanScheduleParams.setOutstandingBalance(outstandingBalance);
// total outstanding balance as per rest for interest calculation.
Money outstandingBalanceAsPerRest = outstandingBalance;
loanScheduleParams.setOutstandingBalanceAsPerRest(outstandingBalanceAsPerRest);
// this is required to update total fee amounts in the
// LoanScheduleModel
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loan.getActiveCharges());
periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms, loanScheduleParams,
chargesDueAtTimeOfDisbursement);
MonetaryCurrency currency = outstandingBalance.getCurrency();
// early payments will be added here and as per the selected
// strategy
// action will be performed on this value
Money reducePrincipal = outstandingBalanceAsPerRest.zero();
Money uncompoundedAmount = outstandingBalanceAsPerRest.zero();
// principal changes will be added along with date(after applying
// rest)
// from when these amounts will effect the outstanding balance for
// interest calculation
final Map<LocalDate, Money> principalPortionMap = new HashMap<>();
// compounding(principal) amounts will be added along with
// date(after applying compounding frequency)
// from when these amounts will effect the outstanding balance for
// interest calculation
final Map<LocalDate, Money> latePaymentMap = new HashMap<>();
// compounding(interest/Fee) amounts will be added along with
// date(after applying compounding frequency)
// from when these amounts will effect the outstanding balance for
// interest calculation
final TreeMap<LocalDate, Money> compoundingMap = new TreeMap<>();
final Map<LocalDate, Map<LocalDate, Money>> compoundingDateVariations = new HashMap<>();
LocalDate currentDate = DateUtils.getBusinessLocalDate();
LocalDate lastRestDate = currentDate;
if (loanApplicationTerms.isInterestBearingAndInterestRecalculationEnabled()) {
lastRestDate = getNextRestScheduleDate(currentDate.minusDays(1), loanApplicationTerms, holidayDetailDTO);
}
LocalDate actualRepaymentDate = RepaymentStartDateType.DISBURSEMENT_DATE
.equals(loanApplicationTerms.getRepaymentStartDateType()) ? loanApplicationTerms.getExpectedDisbursementDate()
: loanApplicationTerms.getSubmittedOnDate();
boolean isFirstRepayment = true;
// cumulative fields
Money totalCumulativePrincipal = principalToBeScheduled.zero();
Money totalCumulativeInterest = principalToBeScheduled.zero();
Money totalFeeChargesCharged = principalToBeScheduled.zero().plus(chargesDueAtTimeOfDisbursement);
Money totalPenaltyChargesCharged = principalToBeScheduled.zero();
Money totalRepaymentExpected;
// Actual period Number as per the schedule
int periodNumber = 1;
// Actual period Number plus interest only repayments
int instalmentNumber = 1;
LocalDate lastInstallmentDate = actualRepaymentDate;
LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType())
? loanApplicationTerms.getExpectedDisbursementDate()
: loanApplicationTerms.getSubmittedOnDate();
// Set fixed Amortization Amounts(either EMI or Principal )
updateAmortization(mc, loanApplicationTerms, periodNumber, outstandingBalance);
// count periods without interest grace to exclude for flat loan
// calculations
final Map<LocalDate, Money> disburseDetailMap = new HashMap<>();
if (loanApplicationTerms.isMultiDisburseLoan()) {
/* fetches the first tranche amount and also updates other tranche details to map */
Money disburseAmt = Money.of(currency,
getDisbursementAmount(loanApplicationTerms, loanApplicationTerms.getExpectedDisbursementDate(), disburseDetailMap,
loanScheduleParams.applyInterestRecalculation()));
Money downPaymentAmt = Money.zero(currency);
if (loanApplicationTerms.isDownPaymentEnabled()) {
downPaymentAmt = Money.of(currency, MathUtil.percentageOf(disburseAmt.getAmount(),
loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19));
if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) {
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, loanApplicationTerms.getInstallmentAmountInMultiplesOf());
}
}
Money remainingPrincipalAmt = disburseAmt.minus(downPaymentAmt);
outstandingBalance = remainingPrincipalAmt;
outstandingBalanceAsPerRest = remainingPrincipalAmt;
principalToBeScheduled = remainingPrincipalAmt;
}
int loanTermInDays = 0;
List<LoanTermVariationsData> exceptionDataList = loanApplicationTerms.getLoanTermVariations().getExceptionData();
final ListIterator<LoanTermVariationsData> exceptionDataListIterator = exceptionDataList.listIterator();
LoanTermVariationParams loanTermVariationParams = null;
// identify retain installments
final List<LoanRepaymentScheduleInstallment> processInstallmentsInstallments = fetchRetainedInstallments(
loan.getRepaymentScheduleInstallments(), rescheduleFrom, currency);
final List<LoanRepaymentScheduleInstallment> newRepaymentScheduleInstallments = new ArrayList<>();
// Block process the installment and creates the period if it falls
// before reschedule from date
// This will create the recalculation details by applying the
// transactions
for (LoanRepaymentScheduleInstallment installment : processInstallmentsInstallments) {
if (installment.isDownPayment()) {
instalmentNumber++;
periods.add(createLoanScheduleModelDownPaymentPeriod(installment, outstandingBalance));
newRepaymentScheduleInstallments.add(installment);
continue;
}
// this will generate the next schedule due date and allows to
// process the installment only if recalculate from date is
// greater than due date
if (DateUtils.isAfter(installment.getDueDate(), lastInstallmentDate)) {
if (totalCumulativePrincipal.isGreaterThanOrEqualTo(loanApplicationTerms.getTotalDisbursedAmount())) {
break;
}
ArrayList<LoanTermVariationsData> dueDateVariationsDataList = new ArrayList<>();
// check for date changes
do {
actualRepaymentDate = getScheduledDateGenerator().generateNextRepaymentDate(actualRepaymentDate,
loanApplicationTerms, isFirstRepayment);
if (!DateUtils.isBefore(actualRepaymentDate, rescheduleFrom)) {
actualRepaymentDate = lastInstallmentDate;
}
isFirstRepayment = false;
LocalDate prevLastInstDate = lastInstallmentDate;
lastInstallmentDate = getScheduledDateGenerator()
.adjustRepaymentDate(actualRepaymentDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate();
LocalDate modifiedLastInstDate = null;
LoanTermVariationsData variation1 = null;
boolean hasDueDateVariation = false;
while (loanApplicationTerms.getLoanTermVariations().hasDueDateVariation(lastInstallmentDate)) {
hasDueDateVariation = true;
LoanTermVariationsData variation = loanApplicationTerms.getLoanTermVariations().nextDueDateVariation();
if (!variation.isSpecificToInstallment()) {
modifiedLastInstDate = variation.getDateValue();
variation1 = variation;
}
}
if (hasDueDateVariation && !DateUtils.isEqual(lastInstallmentDate, installment.getDueDate())
&& !DateUtils.isEqual(modifiedLastInstDate, installment.getDueDate())) {
lastInstallmentDate = prevLastInstDate;
actualRepaymentDate = lastInstallmentDate;
if (modifiedLastInstDate != null) {
loanApplicationTerms.getLoanTermVariations().previousDueDateVariation();
}
} else if (DateUtils.isEqual(modifiedLastInstDate, installment.getDueDate())) {
actualRepaymentDate = modifiedLastInstDate;
lastInstallmentDate = actualRepaymentDate;
dueDateVariationsDataList.add(variation1);
}
loanTermVariationParams = applyExceptionLoanTermVariations(loanApplicationTerms, lastInstallmentDate,
exceptionDataListIterator, instalmentNumber, totalCumulativePrincipal, totalCumulativeInterest, mc);
} while (loanTermVariationParams != null && loanTermVariationParams.skipPeriod());
periodNumber++;
for (LoanTermVariationsData dueDateVariation : dueDateVariationsDataList) {
dueDateVariation.setProcessed(true);
}
if (loanTermVariationParams != null && loanTermVariationParams.skipPeriod()) {
List<LoanTermVariationsData> variationsDataList = loanTermVariationParams.variationsData();
for (LoanTermVariationsData variationsData : variationsDataList) {
variationsData.setProcessed(true);
}
}
}
for (Map.Entry<LocalDate, Money> disburseDetail : disburseDetailMap.entrySet()) {
if (DateUtils.isAfter(disburseDetail.getKey(), installment.getFromDate())
&& !DateUtils.isAfter(disburseDetail.getKey(), installment.getDueDate())) {
// creates and add disbursement detail to the repayments
// period
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod
.disbursement(disburseDetail.getKey(), disburseDetail.getValue(), chargesDueAtTimeOfDisbursement);
periods.add(disbursementPeriod);
BigDecimal downPaymentAmt = BigDecimal.ZERO;
if (loanApplicationTerms.isDownPaymentEnabled()) {
final LoanScheduleModelDownPaymentPeriod downPaymentPeriod = createDownPaymentPeriod(loanApplicationTerms,
loanScheduleParams, disburseDetail.getKey(), disburseDetail.getValue().getAmount());
periods.add(downPaymentPeriod);
downPaymentAmt = downPaymentPeriod.principalDue();
}
// updates actual outstanding balance with new
// disbursement detail
Money remainingPrincipal = disburseDetail.getValue().minus(downPaymentAmt);
outstandingBalance = outstandingBalance.plus(remainingPrincipal);
principalToBeScheduled = principalToBeScheduled.plus(remainingPrincipal);
}
}
// calculation of basic fields to start the schedule generation
// from the middle
periodStartDate = installment.getDueDate();
installment.resetDerivedComponents();
newRepaymentScheduleInstallments.add(installment);
outstandingBalance = outstandingBalance.minus(installment.getPrincipal(currency));
final LoanScheduleModelPeriod loanScheduleModelPeriod = createLoanScheduleModelPeriod(installment, outstandingBalance, mc);
periods.add(loanScheduleModelPeriod);
totalCumulativePrincipal = totalCumulativePrincipal.plus(installment.getPrincipal(currency));
totalCumulativeInterest = totalCumulativeInterest.plus(installment.getInterestCharged(currency));
totalFeeChargesCharged = totalFeeChargesCharged.plus(installment.getFeeChargesCharged(currency));
totalPenaltyChargesCharged = totalPenaltyChargesCharged.plus(installment.getPenaltyChargesCharged(currency));
instalmentNumber++;
loanTermInDays = DateUtils.getExactDifferenceInDays(installment.getFromDate(), installment.getDueDate());
if (loanApplicationTerms.isInterestBearingAndInterestRecalculationEnabled()) {
// populates the collection with transactions till the due
// date
// of
// the period for interest recalculation enabled loans
Collection<RecalculationDetail> applicableTransactions = getApplicableTransactionsForPeriod(applyInterestRecalculation,
installment.getDueDate(), recalculationDetails);
// calculates the expected principal value for this
// repayment
// schedule
Money principalPortionCalculated = principalToBeScheduled.zero();
if (!installment.isRecalculatedInterestComponent()) {
principalPortionCalculated = calculateExpectedPrincipalPortion(installment.getInterestCharged(currency),
loanApplicationTerms);
}
// expected principal considering the previously paid excess
// amount
Money actualPrincipalPortion = principalPortionCalculated.minus(reducePrincipal);
if (actualPrincipalPortion.isLessThanZero()) {
actualPrincipalPortion = principalPortionCalculated.zero();
}
Money unprocessed = updateEarlyPaidAmountsToMap(loanApplicationTerms, holidayDetailDTO,
loanRepaymentScheduleTransactionProcessor, newRepaymentScheduleInstallments, currency, principalPortionMap,
installment, applicableTransactions, actualPrincipalPortion, loan.getActiveCharges());
// this block is to adjust the period number based on the
// actual
// schedule due date and installment due date
// recalculatedInterestComponent installment shouldn't be
// considered while calculating fixed EMI amounts
int period = periodNumber;
if (!DateUtils.isEqual(lastInstallmentDate, installment.getDueDate())) {
period--;
}
reducePrincipal = fetchEarlyPaidAmount(installment.getPrincipal(currency), principalPortionCalculated, reducePrincipal,
loanApplicationTerms, totalCumulativePrincipal, period, mc);
// Updates principal paid map with efective date for
// reducing
// the amount from outstanding balance(interest calculation)
LocalDate amountApplicableDate = null;
if (loanApplicationTerms.getRestCalendarInstance() != null) {
amountApplicableDate = getNextRestScheduleDate(installment.getDueDate().minusDays(1), loanApplicationTerms,
holidayDetailDTO);
}
// updates map with the installment principal amount
// excluding
// unprocessed amount since this amount is already
// accounted.
updateMapWithAmount(principalPortionMap, installment.getPrincipal(currency).minus(unprocessed), amountApplicableDate);
uncompoundedAmount = updateCompoundingDetailsForPartialScheduleGeneration(installment, loanApplicationTerms,
principalPortionMap, compoundingDateVariations, uncompoundedAmount, applicableTransactions, lastRestDate,
holidayDetailDTO);
// update outstanding balance for interest calculation
outstandingBalanceAsPerRest = updateBalanceForInterestCalculation(principalPortionMap, installment.getDueDate(),
outstandingBalanceAsPerRest);
outstandingBalanceAsPerRest = calculateOutstandingBalanceAsPerRest(loanApplicationTerms, disburseDetailMap,
installment.getDueDate(), outstandingBalanceAsPerRest);
// updates the map with over due amounts
updateLatePaymentsToMap(loanApplicationTerms, holidayDetailDTO, currency, latePaymentMap, lastInstallmentDate,
newRepaymentScheduleInstallments, true, lastRestDate);
} else {
outstandingBalanceAsPerRest = outstandingBalance;
}
}
totalRepaymentExpected = totalCumulativePrincipal.plus(totalCumulativeInterest).plus(totalFeeChargesCharged)
.plus(totalPenaltyChargesCharged);
// for partial schedule generation
if (!newRepaymentScheduleInstallments.isEmpty() && totalCumulativeInterest.isGreaterThanZero()) {
Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency);
loanScheduleParams = LoanScheduleParams.createLoanScheduleParamsForPartialUpdate(periodNumber, instalmentNumber,
loanTermInDays, periodStartDate, actualRepaymentDate, totalCumulativePrincipal, totalCumulativeInterest,
totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected,
totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap, compoundingMap,
uncompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest,
newRepaymentScheduleInstallments, recalculationDetails, loanRepaymentScheduleTransactionProcessor, scheduleTillDate,
currency.toData(), applyInterestRecalculation, mc);
retainedInstallments.addAll(newRepaymentScheduleInstallments);
loanScheduleParams.getCompoundingDateVariations().putAll(compoundingDateVariations);
loanApplicationTerms.updateTotalInterestDue(Money.of(currency, loan.getSummary().getTotalInterestCharged()));
} else {
loanApplicationTerms.getLoanTermVariations().resetVariations();
periods.clear();
}
}
if (retainedInstallments.size() > 0
&& retainedInstallments.get(retainedInstallments.size() - 1).getRescheduleInterestPortion() != null) {
loanApplicationTerms.setInterestTobeApproppriated(
Money.of(loan.getCurrency(), retainedInstallments.get(retainedInstallments.size() - 1).getRescheduleInterestPortion()));
}
LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO,
loanScheduleParams);
for (LoanScheduleModelPeriod loanScheduleModelPeriod : loanScheduleModel.getPeriods()) {
if (loanScheduleModelPeriod.isRepaymentPeriod() || loanScheduleModelPeriod.isDownPaymentPeriod()) {
// adding newly created repayment periods to installments
addLoanRepaymentScheduleInstallment(retainedInstallments, loanScheduleModelPeriod);
}
}
periods.addAll(loanScheduleModel.getPeriods());
LoanScheduleModel loanScheduleModelWithPeriodChanges = LoanScheduleModel.withLoanScheduleModelPeriods(periods, loanScheduleModel);
return LoanScheduleDTO.from(retainedInstallments, loanScheduleModelWithPeriodChanges);
}