FINERACT-1981: Prepayment calculation
This commit is contained in:
+9
-8
@@ -59,7 +59,6 @@ import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
@@ -82,7 +81,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParamet
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
@@ -149,7 +148,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
// only for progressive loans
|
||||
public Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessProgressiveLoanTransactions(
|
||||
LocalDate disbursementDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
|
||||
LocalDate disbursementDate, LocalDate currentDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
|
||||
if (loanTransactions.isEmpty()) {
|
||||
@@ -213,7 +212,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId);
|
||||
createNewTransaction(oldTransaction, newTransaction, ctx);
|
||||
}
|
||||
recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx);
|
||||
recalculateInterestForDate(currentDate, ctx);
|
||||
List<LoanTransaction> txs = changeOperations.stream() //
|
||||
.filter(ChangeOperation::isTransaction) //
|
||||
.map(e -> e.getLoanTransaction().get()).toList();
|
||||
@@ -248,7 +247,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
@Override
|
||||
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft();
|
||||
LocalDate currentDate = DateUtils.getBusinessLocalDate();
|
||||
return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges)
|
||||
.getLeft();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -1590,11 +1591,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
private void updateRepaymentPeriodBalances(PaymentAllocationType paymentAllocationType,
|
||||
LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveLoanInterestScheduleModel model, LocalDate payDate) {
|
||||
PayableDetails payableDetails = emiCalculator.getPayableDetails(model, inAdvanceInstallment.getDueDate(), payDate);
|
||||
PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(model, inAdvanceInstallment.getDueDate(), payDate);
|
||||
|
||||
switch (paymentAllocationType) {
|
||||
case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getPayableInterest().getAmount());
|
||||
case IN_ADVANCE_PRINCIPAL -> inAdvanceInstallment.updatePrincipal(payableDetails.getPayablePrincipal().getAmount());
|
||||
case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getDueInterest().getAmount());
|
||||
case IN_ADVANCE_PRINCIPAL -> inAdvanceInstallment.updatePrincipal(payableDetails.getDuePrincipal().getAmount());
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
@Data
|
||||
public class OutstandingDetails {
|
||||
|
||||
private final Money outstandingPrincipal;
|
||||
private final Money outstandingInterest;
|
||||
}
|
||||
+3
-4
@@ -22,10 +22,9 @@ import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
@Data
|
||||
public class PayableDetails {
|
||||
public class PeriodDueDetails {
|
||||
|
||||
private final Money emi;
|
||||
private final Money payablePrincipal;
|
||||
private final Money payableInterest;
|
||||
private final Money outstandingBalance;
|
||||
private final Money duePrincipal;
|
||||
private final Money dueInterest;
|
||||
}
|
||||
+17
@@ -197,4 +197,21 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
return Money.zero(loanProductRelatedDetail.getCurrency(), mc);
|
||||
}
|
||||
|
||||
public Money getTotalDueInterest() {
|
||||
return repaymentPeriods().stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest))
|
||||
.reduce(getZero(), Money::plus);
|
||||
}
|
||||
|
||||
public Money getTotalDuePrincipal() {
|
||||
return repaymentPeriods.stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
|
||||
.reduce(getZero(), Money::plus);
|
||||
}
|
||||
|
||||
public Money getTotalPaidInterest() {
|
||||
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidInterest).reduce(getZero(), Money::plus);
|
||||
}
|
||||
|
||||
public Money getTotalPaidPrincipal() {
|
||||
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidPrincipal).reduce(getZero(), Money::plus);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-9
@@ -46,7 +46,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleD
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
@@ -195,7 +195,8 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
continue;
|
||||
}
|
||||
|
||||
Money outstandingBalance = emiCalculator.getOutstandingLoanBalance(interestScheduleModel, periodDueDate, disbursementDate);
|
||||
Money outstandingBalance = emiCalculator.getOutstandingLoanBalanceOfPeriod(interestScheduleModel, periodDueDate,
|
||||
disbursementDate);
|
||||
|
||||
final Money disbursedAmount = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal(), mc);
|
||||
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod
|
||||
@@ -263,17 +264,14 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
|
||||
};
|
||||
|
||||
ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(),
|
||||
ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), onDate,
|
||||
loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight();
|
||||
|
||||
PayableDetails result = emiCalculator.getPayableDetails(model, actualInstallment.getDueDate(), transactionDate);
|
||||
OutstandingDetails result = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate);
|
||||
// TODO: We should add all the past due outstanding amounts as well
|
||||
OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) //
|
||||
.principal(result.getOutstandingBalance()) //
|
||||
.interest(result.getPayableInterest());
|
||||
|
||||
installments.stream().filter(installment -> installment.getDueDate().isBefore(onDate))
|
||||
.forEach(installment -> amounts.plusInterest(installment.getInterestOutstanding(currency)));
|
||||
.principal(result.getOutstandingPrincipal()) //
|
||||
.interest(result.getOutstandingInterest());//
|
||||
|
||||
installments.forEach(installment -> amounts //
|
||||
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
|
||||
|
||||
+6
-3
@@ -25,7 +25,8 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
@@ -55,8 +56,10 @@ public interface EMICalculator {
|
||||
void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
|
||||
Money principalAmount);
|
||||
|
||||
PayableDetails getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate payDate);
|
||||
PeriodDueDetails getDueAmounts(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate targetDate);
|
||||
|
||||
Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
LocalDate targetDate);
|
||||
|
||||
OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel model, LocalDate targetDate);
|
||||
}
|
||||
|
||||
+67
-21
@@ -35,11 +35,13 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -156,8 +158,63 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
final LocalDate repaymentPeriodDueDate, final LocalDate targetDate) {
|
||||
public PeriodDueDetails getDueAmounts(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate,
|
||||
final LocalDate targetDate) {
|
||||
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
|
||||
repaymentPeriodDueDate, targetDate);
|
||||
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow();
|
||||
boolean multiplePeriodIsUnpaid = recalculatedScheduleModelTillDate.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
|
||||
.count() > 1L;
|
||||
if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) {
|
||||
repaymentPeriod.setEmi(repaymentPeriod.getOriginalEmi());
|
||||
}
|
||||
|
||||
return new PeriodDueDetails(repaymentPeriod.getEmi(), //
|
||||
repaymentPeriod.getDuePrincipal(), //
|
||||
repaymentPeriod.getDueInterest()); //
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
LocalDate targetDate) {
|
||||
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
|
||||
repaymentPeriodDueDate, targetDate);
|
||||
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow();
|
||||
|
||||
return repaymentPeriod.getOutstandingLoanBalance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate targetDate) {
|
||||
MathContext mc = scheduleModel.mc();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
|
||||
|
||||
scheduleModelCopy.repaymentPeriods().stream()//
|
||||
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream()//
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) //
|
||||
.reduce((one, two) -> two))
|
||||
.ifPresent(ip -> ip.setDueDate(targetDate)); //
|
||||
|
||||
calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy);
|
||||
scheduleModelCopy.repaymentPeriods()
|
||||
.forEach(rp -> rp.getInterestPeriods().stream().filter(ip -> targetDate.isBefore(ip.getDueDate())).forEach(ip -> {
|
||||
ip.setRateFactor(BigDecimal.ZERO);
|
||||
ip.setRateFactorTillPeriodDueDate(BigDecimal.ZERO);
|
||||
}));
|
||||
calculateOutstandingBalance(scheduleModelCopy);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy);
|
||||
|
||||
Money totalOutstandingPrincipal = MathUtil
|
||||
.negativeToZero(scheduleModelCopy.getTotalDuePrincipal().minus(scheduleModelCopy.getTotalPaidPrincipal()));
|
||||
Money totalOutstandingInterest = MathUtil
|
||||
.negativeToZero(scheduleModelCopy.getTotalDueInterest().minus(scheduleModelCopy.getTotalPaidInterest()));
|
||||
return new OutstandingDetails(totalOutstandingPrincipal, totalOutstandingInterest);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
LocalDate repaymentPeriodDueDate, LocalDate targetDate) {
|
||||
MathContext mc = scheduleModel.mc();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
|
||||
RepaymentPeriod repaymentPeriod = scheduleModelCopy.repaymentPeriods().stream()
|
||||
@@ -175,11 +232,12 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
interestPeriod = repaymentPeriod.getInterestPeriods().stream()
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())).findFirst().orElseThrow();
|
||||
}
|
||||
scheduleModelCopy.repaymentPeriods().stream()
|
||||
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()
|
||||
.ifPresent(rp -> rp.getInterestPeriods().stream()
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate()))
|
||||
.reduce((one, two) -> two).ifPresent(ip -> ip.setDueDate(targetDate)));
|
||||
scheduleModelCopy.repaymentPeriods().stream()//
|
||||
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream()//
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) //
|
||||
.reduce((one, two) -> two))
|
||||
.ifPresent(ip -> ip.setDueDate(targetDate)); //
|
||||
interestPeriod.setDueDate(adjustedTargetDate);
|
||||
int index = repaymentPeriod.getInterestPeriods().indexOf(interestPeriod);
|
||||
repaymentPeriod.getInterestPeriods().subList(index + 1, repaymentPeriod.getInterestPeriods().size()).clear();
|
||||
@@ -188,19 +246,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
calculateOutstandingBalance(scheduleModelCopy);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy);
|
||||
|
||||
boolean multiplePeriodIsUnpaid = scheduleModelCopy.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()).count() > 1L;
|
||||
if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) {
|
||||
repaymentPeriod.setEmi(repaymentPeriod.getOriginalEmi());
|
||||
}
|
||||
|
||||
return new PayableDetails(repaymentPeriod.getEmi(), repaymentPeriod.getDuePrincipal(), repaymentPeriod.getDueInterest(),
|
||||
interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount(), mc));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
LocalDate targetDate) {
|
||||
return getPayableDetails(interestScheduleModel, repaymentPeriodDueDate, targetDate).getOutstandingBalance();
|
||||
return scheduleModelCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user