FINERACT-2354: First step - basic implementation of re-aging for Interest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change
This commit is contained in:
committed by
Adam Saghy
parent
23c1b2b8af
commit
d21a1a98a2
+213
-67
@@ -47,6 +47,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -97,6 +98,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.domain.LoanApplicationTerms;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
|
||||
@@ -2890,76 +2892,149 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
||||
|
||||
AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency));
|
||||
installments.forEach(i -> {
|
||||
Money principalOutstanding = i.getPrincipalOutstanding(currency);
|
||||
if (principalOutstanding.isGreaterThanZero()) {
|
||||
outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding));
|
||||
i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated());
|
||||
// re-aging logic for interest-bearing loans
|
||||
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
|
||||
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
handleReAgeWithInterestRecalculationEnabled(loanTransaction, progressiveTransactionCtx);
|
||||
} else if (loanTransaction.getLoan().isInterestBearing() && !loanTransaction.getLoan().isInterestRecalculationEnabled()) {
|
||||
// TODO: implement interestRecalculation = false logic
|
||||
throw new NotImplementedException(
|
||||
"Logic for re-aging when interest bearing loan has interestRecalculation disabled is not implemented");
|
||||
} else {
|
||||
AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency));
|
||||
installments.forEach(i -> {
|
||||
Money principalOutstanding = i.getPrincipalOutstanding(currency);
|
||||
if (principalOutstanding.isGreaterThanZero()) {
|
||||
outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding));
|
||||
i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated());
|
||||
}
|
||||
});
|
||||
|
||||
loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), Money.zero(currency), Money.zero(currency),
|
||||
Money.zero(currency));
|
||||
|
||||
if (outstandingPrincipalBalance.get().isZero()) {
|
||||
loanTransaction.reverse();
|
||||
return;
|
||||
}
|
||||
|
||||
Money calculatedPrincipal = Money.zero(currency);
|
||||
Money adjustCalculatedPrincipal = Money.zero(currency);
|
||||
if (outstandingPrincipalBalance.get().isGreaterThanZero()) {
|
||||
calculatedPrincipal = outstandingPrincipalBalance.get()
|
||||
.dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getMathContext());
|
||||
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProductRelatedDetail()
|
||||
.getInstallmentAmountInMultiplesOf();
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf);
|
||||
}
|
||||
adjustCalculatedPrincipal = outstandingPrincipalBalance.get()
|
||||
.minus(calculatedPrincipal.multipliedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()));
|
||||
}
|
||||
|
||||
Optional<LoanRepaymentScheduleInstallment> lastNormalInstallmentOptional = installments.stream().filter(i -> !i.isDownPayment())
|
||||
.filter(i -> i.getDueDate().isBefore(loanTransaction.getTransactionDate())).reduce((first, second) -> second);
|
||||
|
||||
int reAgedInstallmentNumber;
|
||||
LocalDate fromDate;
|
||||
Loan loan;
|
||||
if (lastNormalInstallmentOptional.isEmpty()) {
|
||||
LoanRepaymentScheduleInstallment firstNormalInstallment = installments.stream().filter(i -> !i.isDownPayment())
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).orElseThrow();
|
||||
reAgedInstallmentNumber = firstNormalInstallment.getInstallmentNumber();
|
||||
fromDate = firstNormalInstallment.getFromDate();
|
||||
loan = firstNormalInstallment.getLoan();
|
||||
} else {
|
||||
LoanRepaymentScheduleInstallment lastNormalInstallment = lastNormalInstallmentOptional.get();
|
||||
reAgedInstallmentNumber = lastNormalInstallment.getInstallmentNumber() + 1;
|
||||
fromDate = lastNormalInstallment.getDueDate();
|
||||
loan = lastNormalInstallment.getLoan();
|
||||
}
|
||||
|
||||
LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan,
|
||||
reAgedInstallmentNumber, fromDate, loanTransaction.getLoanReAgeParameter().getStartDate(),
|
||||
calculatedPrincipal.getAmount());
|
||||
insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate());
|
||||
|
||||
for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) {
|
||||
LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(),
|
||||
reAgedInstallment.getDueDate());
|
||||
int nextReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber() + 1;
|
||||
reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(),
|
||||
nextReAgedInstallmentNumber, reAgedInstallment.getDueDate(), calculatedDueDate, calculatedPrincipal.getAmount());
|
||||
if (i + 1 == loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) {
|
||||
reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal);
|
||||
}
|
||||
insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate());
|
||||
}
|
||||
int lastReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber();
|
||||
List<LoanRepaymentScheduleInstallment> toRemove = installments.stream().filter(i -> i != null && !i.isAdditional()
|
||||
&& i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber).toList();
|
||||
toRemove.forEach(installments::remove);
|
||||
reprocessInstallments(installments);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupInstallmentsAndRepaymentPeriodsAfterReAging(final ProgressiveTransactionCtx ctx) {
|
||||
final List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
||||
final List<RepaymentPeriod> repaymentPeriods = ctx.getModel().repaymentPeriods();
|
||||
|
||||
// Find the last re-aged installment number
|
||||
final OptionalInt lastReAgedInstallmentNumberOpt = installments.stream().filter(LoanRepaymentScheduleInstallment::isReAged)
|
||||
.mapToInt(LoanRepaymentScheduleInstallment::getInstallmentNumber).max();
|
||||
|
||||
if (lastReAgedInstallmentNumberOpt.isPresent()) {
|
||||
final int lastReAgedInstallmentNumber = lastReAgedInstallmentNumberOpt.getAsInt();
|
||||
final LoanRepaymentScheduleInstallment lastReAgedInstallment = installments.stream()
|
||||
.filter(i -> i.getInstallmentNumber().equals(lastReAgedInstallmentNumber)).findFirst().orElse(null);
|
||||
// Remove installments with numbers greater than the last re-aged installment
|
||||
final List<LoanRepaymentScheduleInstallment> installmentsToRemove = installments.stream().filter(i -> i != null
|
||||
&& !i.isAdditional() && i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber)
|
||||
.toList();
|
||||
if (lastReAgedInstallment != null) {
|
||||
final List<RepaymentPeriod> repaymentPeriodsToRemove = repaymentPeriods.stream()
|
||||
.filter(rp -> !rp.getFromDate().isBefore(lastReAgedInstallment.getDueDate())).toList();
|
||||
repaymentPeriodsToRemove.forEach(repaymentPeriods::remove);
|
||||
}
|
||||
installmentsToRemove.forEach(installments::remove);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateInstallmentsByModelForReAging(final LoanTransaction loanTransaction, final ProgressiveTransactionCtx ctx) {
|
||||
ctx.getModel().repaymentPeriods().forEach(rp -> {
|
||||
final LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream().filter(
|
||||
ri -> ri.getFromDate().equals(rp.getFromDate()) && ri.getDueDate().equals(rp.getDueDate()) && !ri.isDownPayment())
|
||||
.findFirst().orElse(null);
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
if (installment != null) {
|
||||
installment.setFromDate(rp.getFromDate());
|
||||
installment.setDueDate(rp.getDueDate());
|
||||
|
||||
if (rp.getEmi().isZero()) {
|
||||
installment.updatePrincipal(BigDecimal.ZERO);
|
||||
installment.updateInterestCharged(BigDecimal.ZERO);
|
||||
} else {
|
||||
installment.updatePrincipal(rp.getDuePrincipal().getAmount());
|
||||
installment.updateInterestCharged(rp.getDueInterest().getAmount());
|
||||
}
|
||||
installment.setReAged(true);
|
||||
installment.updateObligationsMet(ctx.getCurrency(), transactionDate);
|
||||
} else {
|
||||
final LoanRepaymentScheduleInstallment lastInstallment = ctx.getInstallments().getLast();
|
||||
final LoanRepaymentScheduleInstallment newInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(
|
||||
loanTransaction.getLoan(), lastInstallment.getInstallmentNumber() + 1, rp.getFromDate(), rp.getDueDate(),
|
||||
rp.getDuePrincipal().getAmount());
|
||||
|
||||
if (rp.getDueInterest().isGreaterThanZero()) {
|
||||
newInstallment.addToInterest(transactionDate, rp.getDueInterest());
|
||||
}
|
||||
|
||||
newInstallment.updateObligationsMet(ctx.getCurrency(), transactionDate);
|
||||
ctx.getInstallments().add(newInstallment);
|
||||
}
|
||||
});
|
||||
|
||||
loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), Money.zero(currency), Money.zero(currency),
|
||||
Money.zero(currency));
|
||||
|
||||
if (outstandingPrincipalBalance.get().isZero()) {
|
||||
loanTransaction.reverse();
|
||||
return;
|
||||
}
|
||||
|
||||
Money calculatedPrincipal = Money.zero(currency);
|
||||
Money adjustCalculatedPrincipal = Money.zero(currency);
|
||||
if (outstandingPrincipalBalance.get().isGreaterThanZero()) {
|
||||
calculatedPrincipal = outstandingPrincipalBalance.get()
|
||||
.dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getMathContext());
|
||||
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProductRelatedDetail()
|
||||
.getInstallmentAmountInMultiplesOf();
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf);
|
||||
}
|
||||
adjustCalculatedPrincipal = outstandingPrincipalBalance.get()
|
||||
.minus(calculatedPrincipal.multipliedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()));
|
||||
}
|
||||
|
||||
Optional<LoanRepaymentScheduleInstallment> lastNormalInstallmentOptional = installments.stream().filter(i -> !i.isDownPayment())
|
||||
.filter(i -> i.getDueDate().isBefore(loanTransaction.getTransactionDate())).reduce((first, second) -> second);
|
||||
|
||||
int reAgedInstallmentNumber;
|
||||
LocalDate fromDate;
|
||||
Loan loan;
|
||||
if (lastNormalInstallmentOptional.isEmpty()) {
|
||||
LoanRepaymentScheduleInstallment firstNormalInstallment = installments.stream().filter(i -> !i.isDownPayment())
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).orElseThrow();
|
||||
reAgedInstallmentNumber = firstNormalInstallment.getInstallmentNumber();
|
||||
fromDate = firstNormalInstallment.getFromDate();
|
||||
loan = firstNormalInstallment.getLoan();
|
||||
} else {
|
||||
LoanRepaymentScheduleInstallment lastNormalInstallment = lastNormalInstallmentOptional.get();
|
||||
reAgedInstallmentNumber = lastNormalInstallment.getInstallmentNumber() + 1;
|
||||
fromDate = lastNormalInstallment.getDueDate();
|
||||
loan = lastNormalInstallment.getLoan();
|
||||
}
|
||||
|
||||
LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan,
|
||||
reAgedInstallmentNumber, fromDate, loanTransaction.getLoanReAgeParameter().getStartDate(), calculatedPrincipal.getAmount());
|
||||
insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate());
|
||||
|
||||
for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) {
|
||||
LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(),
|
||||
reAgedInstallment.getDueDate());
|
||||
int nextReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber() + 1;
|
||||
reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(),
|
||||
nextReAgedInstallmentNumber, reAgedInstallment.getDueDate(), calculatedDueDate, calculatedPrincipal.getAmount());
|
||||
if (i + 1 == loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) {
|
||||
reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal);
|
||||
}
|
||||
insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate());
|
||||
}
|
||||
int lastReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber();
|
||||
List<LoanRepaymentScheduleInstallment> toRemove = installments.stream().filter(i -> i != null && !i.isAdditional()
|
||||
&& i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber).toList();
|
||||
toRemove.forEach(installments::remove);
|
||||
reprocessInstallments(installments);
|
||||
cleanupInstallmentsAndRepaymentPeriodsAfterReAging(ctx);
|
||||
}
|
||||
|
||||
private void reprocessInstallments(final List<LoanRepaymentScheduleInstallment> installments) {
|
||||
@@ -3211,4 +3286,75 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReAgeWithInterestRecalculationEnabled(final LoanTransaction loanTransaction, final ProgressiveTransactionCtx ctx) {
|
||||
final MonetaryCurrency currency = ctx.getCurrency();
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
||||
final List<RepaymentPeriod> repaymentPeriods = ctx.getModel().repaymentPeriods();
|
||||
final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate();
|
||||
|
||||
final List<RepaymentPeriod> periodsBeforeReAging = repaymentPeriods.stream()
|
||||
.filter(rp -> rp.getFromDate().isBefore(reAgingStartDate) && !rp.isFullyPaid()).toList();
|
||||
|
||||
final RepaymentPeriod lastPeriod = periodsBeforeReAging.getLast();
|
||||
|
||||
if (!lastPeriod.getDueDate().isEqual(reAgingStartDate)) {
|
||||
// TODO: implement logic when re-aging changes the due dates
|
||||
throw new NotImplementedException("Logic when re-aging changes the due dates not implemented");
|
||||
}
|
||||
|
||||
final BigDecimal interestBeforeReAging = emiCalculator
|
||||
.getPeriodInterestTillDate(ctx.getModel(), lastPeriod.getDueDate(), transactionDate, false).getAmount();
|
||||
|
||||
final AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency));
|
||||
installments.forEach(i -> {
|
||||
final Money principalOutstanding = i.getPrincipalOutstanding(currency);
|
||||
if (principalOutstanding.isGreaterThanZero()) {
|
||||
outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding));
|
||||
}
|
||||
});
|
||||
|
||||
final AtomicReference<BigDecimal> interestFromZeroedInstallments = new AtomicReference<>(interestBeforeReAging);
|
||||
|
||||
installments.stream()
|
||||
.filter(installment -> !installment.isObligationsMet() && !installment.getDueDate().isAfter(lastPeriod.getFromDate()))
|
||||
.forEach(installment -> {
|
||||
final BigDecimal currentInterest = interestFromZeroedInstallments.get();
|
||||
final BigDecimal additionalInterest = MathUtil.nullToZero(installment.getInterestOutstanding(currency).getAmount()
|
||||
.add(MathUtil.nullToZero(installment.getCreditedInterest()).negate()));
|
||||
interestFromZeroedInstallments.set(currentInterest.add(additionalInterest));
|
||||
});
|
||||
|
||||
final BigDecimal interestRate = loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate();
|
||||
|
||||
final LoanApplicationTerms loanApplicationTerms = new LoanApplicationTerms.Builder().currency(currency.getCurrencyData())
|
||||
.repaymentsStartingFromDate(reAgingStartDate).principal(outstandingPrincipalBalance.get())
|
||||
.loanTermFrequency(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())
|
||||
.loanTermPeriodFrequencyType(loanTransaction.getLoanReAgeParameter().getFrequencyType())
|
||||
.numberOfRepayments(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())
|
||||
.repaymentEvery(loanTransaction.getLoanReAgeParameter().getFrequencyNumber())
|
||||
.repaymentPeriodFrequencyType(loanTransaction.getLoanReAgeParameter().getFrequencyType())
|
||||
.interestRatePerPeriod(interestRate)
|
||||
.interestRatePeriodFrequencyType(loan.getLoanRepaymentScheduleDetail().getRepaymentPeriodFrequencyType())
|
||||
.annualNominalInterestRate(interestRate).daysInMonthType(loan.getLoanProduct().fetchDaysInMonthType())
|
||||
.daysInYearType(loan.getLoanProduct().fetchDaysInYearType()).inArrearsTolerance(Money.zero(currency, mc))
|
||||
.isDownPaymentEnabled(false).downPaymentPercentage(ZERO).seedDate(reAgingStartDate)
|
||||
.interestRecognitionOnDisbursementDate(
|
||||
loan.getLoanProduct().getLoanProductRelatedDetail().isInterestRecognitionOnDisbursementDate())
|
||||
.daysInYearCustomStrategy(loan.getLoanProduct().getLoanProductRelatedDetail().getDaysInYearCustomStrategy())
|
||||
.interestMethod(loan.getLoanProductRelatedDetail().getInterestMethod()).allowPartialPeriodInterestCalculation(
|
||||
loan.getLoanProduct().getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation())
|
||||
.mc(mc).build();
|
||||
|
||||
// Update the existing model with re-aged periods
|
||||
emiCalculator.updateModelRepaymentPeriodsDuringReAge(ctx.getModel(), loanTransaction, loanApplicationTerms, mc);
|
||||
updateInstallmentsByModelForReAging(loanTransaction, ctx);
|
||||
|
||||
loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(),
|
||||
Money.of(currency, interestFromZeroedInstallments.get()), Money.zero(currency), Money.zero(currency));
|
||||
reprocessInstallments(installments);
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -27,6 +27,8 @@ import java.util.Optional;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
||||
@@ -140,4 +142,7 @@ public interface EMICalculator {
|
||||
* interest paused.
|
||||
*/
|
||||
void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate);
|
||||
|
||||
void updateModelRepaymentPeriodsDuringReAge(ProgressiveLoanInterestScheduleModel scheduleModel, LoanTransaction loanTransaction,
|
||||
LoanApplicationTerms loanApplicationTerms, MathContext mc);
|
||||
}
|
||||
|
||||
+130
@@ -40,6 +40,7 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
|
||||
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
|
||||
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
||||
@@ -47,7 +48,11 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiAdjustment;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
|
||||
@@ -66,6 +71,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
private static final BigDecimal DIVISOR_100 = new BigDecimal("100");
|
||||
private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7);
|
||||
|
||||
private final ScheduledDateGenerator scheduledDateGenerator;
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
|
||||
@@ -559,6 +566,129 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
final LoanTransaction loanTransaction, final LoanApplicationTerms loanApplicationTerms, final MathContext mc) {
|
||||
final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
|
||||
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods();
|
||||
|
||||
moveOutstandingAmountsFromPeriodsBeforeReAging(existingRepaymentPeriods, reAgingStartDate);
|
||||
|
||||
final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate(loanReAgeParameter);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryReAgedScheduleModel(loanApplicationTerms,
|
||||
mc, periodStartDate, transactionDate);
|
||||
|
||||
mergeNewInterestScheduleModelWithExistingOne(scheduleModel, temporaryReAgedScheduleModel, loanTransaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* * Merging the new temporary model of re-aged repayment periods and existing one together. After that recalculate
|
||||
* the balances of the updated model and also recalculate the EMI if the EMI of the last repayment period differs
|
||||
* significantly from other periods.
|
||||
*/
|
||||
private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel, final LoanTransaction loanTransaction) {
|
||||
final List<RepaymentPeriod> newPeriods = temporaryReAgedScheduleModel.repaymentPeriods();
|
||||
|
||||
if (newPeriods.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods();
|
||||
final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate();
|
||||
|
||||
final Optional<RepaymentPeriod> firstExistingRepaymentPeriodOpt = existingRepaymentPeriods.stream()
|
||||
.filter(period -> period.getDueDate().equals(reAgingStartDate)).findFirst();
|
||||
|
||||
for (final RepaymentPeriod newPeriod : newPeriods) {
|
||||
final Optional<RepaymentPeriod> existingRepaymentPeriodOpt = existingRepaymentPeriods.stream().filter(
|
||||
period -> period.getFromDate().equals(newPeriod.getFromDate()) && period.getDueDate().equals(newPeriod.getDueDate()))
|
||||
.findFirst();
|
||||
Optional<RepaymentPeriod> previousExistingRepaymentPeriodOpt = Optional.empty();
|
||||
if (existingRepaymentPeriodOpt.isPresent() && firstExistingRepaymentPeriodOpt.isPresent()
|
||||
&& existingRepaymentPeriodOpt.get().equals(firstExistingRepaymentPeriodOpt.get())) {
|
||||
previousExistingRepaymentPeriodOpt = existingRepaymentPeriodOpt.get().getPrevious();
|
||||
}
|
||||
|
||||
final Money newPrincipal = newPeriod.getDuePrincipal();
|
||||
final Money newInterest = newPeriod.getDueInterest();
|
||||
|
||||
final RepaymentPeriod rp = RepaymentPeriod.create(
|
||||
previousExistingRepaymentPeriodOpt.orElseGet(existingRepaymentPeriods::getLast), newPeriod.getFromDate(),
|
||||
newPeriod.getDueDate(), newPrincipal.add(newInterest), MoneyHelper.getMathContext(),
|
||||
loanTransaction.getLoan().getLoanProductRelatedDetail());
|
||||
rp.setTotalDisbursedAmount(scheduleModel.repaymentPeriods().getFirst().getTotalDisbursedAmount());
|
||||
|
||||
existingRepaymentPeriodOpt.ifPresent(existingRepaymentPeriods::remove);
|
||||
existingRepaymentPeriods.add(rp);
|
||||
calculateRateFactorForRepaymentPeriod(rp, scheduleModel);
|
||||
}
|
||||
|
||||
final RepaymentPeriod lastReAgedInstallment = newPeriods.getLast();
|
||||
final List<RepaymentPeriod> reAgedRepaymentPeriods = existingRepaymentPeriods.stream()
|
||||
.filter(repaymentPeriod -> (!repaymentPeriod.getFromDate().isBefore(reAgingStartDate)
|
||||
|| repaymentPeriod.getDueDate().isEqual(reAgingStartDate))
|
||||
&& !repaymentPeriod.getDueDate().isAfter(lastReAgedInstallment.getDueDate()))
|
||||
.toList();
|
||||
|
||||
calculateOutstandingBalance(scheduleModel);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, loanTransaction.getTransactionDate());
|
||||
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, reAgedRepaymentPeriods);
|
||||
}
|
||||
|
||||
/**
|
||||
* * Generates temporary interestScheduleModel with re-aged repayment periods
|
||||
*/
|
||||
@NotNull
|
||||
private ProgressiveLoanInterestScheduleModel generateTemporaryReAgedScheduleModel(final LoanApplicationTerms loanApplicationTerms,
|
||||
final MathContext mc, final LocalDate periodStartDate, final LocalDate transactionDate) {
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
|
||||
periodStartDate, loanApplicationTerms, null);
|
||||
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetailMinimumData(), null,
|
||||
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
|
||||
|
||||
addDisbursement(temporaryReAgedScheduleModel, EmiChangeOperation.disburse(transactionDate, loanApplicationTerms.getPrincipal()));
|
||||
return temporaryReAgedScheduleModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* * Based on the re-aging start date and frequency data calculates start date for the first re-aged period, which
|
||||
* is used to generate re-aged repayment periods
|
||||
*/
|
||||
@NotNull
|
||||
private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanReAgeParameter loanReAgeParameter) {
|
||||
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
|
||||
return switch (loanReAgeParameter.getFrequencyType()) {
|
||||
case DAYS -> reAgingStartDate.minusDays(loanReAgeParameter.getFrequencyNumber());
|
||||
case WEEKS -> reAgingStartDate.minusWeeks(loanReAgeParameter.getFrequencyNumber());
|
||||
case MONTHS -> reAgingStartDate.minusMonths(loanReAgeParameter.getFrequencyNumber());
|
||||
case YEARS -> reAgingStartDate.minusYears(loanReAgeParameter.getFrequencyNumber());
|
||||
case WHOLE_TERM -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WHOLE_TERM");
|
||||
case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID");
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* * Zeroing out the EMI of the repayment periods, that are before re-aging and not been fully paid. And decreases
|
||||
* the balance correction amount (added during interest recalculation for the business date) by the amount of the
|
||||
* principal that was moved.
|
||||
*/
|
||||
private static void moveOutstandingAmountsFromPeriodsBeforeReAging(final List<RepaymentPeriod> existingRepaymentPeriods,
|
||||
final LocalDate reAgingStartDate) {
|
||||
final List<RepaymentPeriod> periodsBeforeReAging = existingRepaymentPeriods.stream()
|
||||
.filter(rp -> rp.getFromDate().isBefore(reAgingStartDate) && !rp.isFullyPaid()).toList();
|
||||
|
||||
periodsBeforeReAging.forEach(rp -> {
|
||||
final InterestPeriod lastInterestPeriod = rp.getInterestPeriods().getLast();
|
||||
lastInterestPeriod.addBalanceCorrectionAmount(rp.getOutstandingPrincipal().negated());
|
||||
rp.setEmi(rp.getTotalPaidAmount());
|
||||
});
|
||||
}
|
||||
|
||||
private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate tillDate) {
|
||||
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
|
||||
.reduce((first, second) -> second);
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LoanScheduleGeneratorTest {
|
||||
|
||||
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
|
||||
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(mock(ScheduledDateGenerator.class));
|
||||
private static final ApplicationCurrency APPLICATION_CURRENCY = new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$");
|
||||
private static final CurrencyData CURRENCY = APPLICATION_CURRENCY.toData();
|
||||
private static final BigDecimal DISBURSEMENT_AMOUNT = BigDecimal.valueOf(192.22);
|
||||
|
||||
+4
-1
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanproduct.calc;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
@@ -39,6 +41,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestScheduleModelParserServiceGsonImpl;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
||||
@@ -64,7 +67,7 @@ import org.springframework.lang.NonNull;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ProgressiveEMICalculatorTest {
|
||||
|
||||
private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
|
||||
private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(mock(ScheduledDateGenerator.class));
|
||||
|
||||
private static MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class);
|
||||
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
|
||||
|
||||
Reference in New Issue
Block a user