FINERACT-2354: Third step - backdated re-age edge case for Interest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change

This commit is contained in:
mariiaKraievska
2025-11-13 08:44:22 +01:00
committed by Adam Saghy
parent 01ca515e8f
commit 5a1c6edbef
7 changed files with 438 additions and 141 deletions
@@ -1578,49 +1578,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments());
}
private List<RepaymentPeriod> findPossiblyOverdueRepaymentPeriods(LocalDate targetDate, ProgressiveLoanInterestScheduleModel model) {
return model.repaymentPeriods().stream() //
.filter(repaymentPeriod -> DateUtils.isAfter(targetDate, repaymentPeriod.getDueDate())).toList();
}
public boolean rework(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
boolean hasChange = false;
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
List<RepaymentPeriod> overdueInstallmentsSortedByInstallmentNumber = findPossiblyOverdueRepaymentPeriods(targetDate, model);
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
RepaymentPeriod lastPeriod = model.getLastRepaymentPeriod();
RepaymentPeriod currentPeriod = model.findRepaymentPeriod(targetDate).orElse(lastPeriod);
MonetaryCurrency currency = model.zero().getCurrency();
Money overDuePrincipal = Money.zero(currency);
Money aggregatedOverDuePrincipal = Money.zero(currency);
for (RepaymentPeriod processingPeriod : overdueInstallmentsSortedByInstallmentNumber) {
// add and subtract outstanding principal
if (!overDuePrincipal.isZero()) {
boolean currentChanges = adjustOverduePrincipal(targetDate, processingPeriod, overDuePrincipal,
aggregatedOverDuePrincipal, ctx);
hasChange = hasChange || currentChanges;
}
overDuePrincipal = processingPeriod.getOutstandingPrincipal();
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
}
if (!currentPeriod.equals(lastPeriod) || !targetDate.isAfter(lastPeriod.getDueDate())) {
boolean currentChanges = adjustOverduePrincipal(targetDate, currentPeriod, overDuePrincipal, aggregatedOverDuePrincipal,
ctx);
hasChange = hasChange || currentChanges;
}
if (aggregatedOverDuePrincipal.isGreaterThanZero()
&& (model.lastOverdueBalanceChange() == null || model.lastOverdueBalanceChange().isBefore(targetDate))) {
model.lastOverdueBalanceChange(targetDate);
}
}
return hasChange;
}
public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
recalculateInterestForDate(targetDate, ctx, true);
}
@@ -1631,7 +1588,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (isInterestRecalculationSupported(ctx, loan) && !loan.isNpa()
&& !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
boolean modelHasUpdates = rework(targetDate, ctx);
boolean modelHasUpdates = emiCalculator.recalculateModelOverdueAmountsTillDate(ctx, targetDate);
if (modelHasUpdates && updateInstallments) {
updateInstallmentsPrincipalAndInterestByModel(ctx);
}
@@ -1639,38 +1596,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
private boolean adjustOverduePrincipal(LocalDate currentDate, RepaymentPeriod currentInstallment, Money overduePrincipal,
Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) {
LocalDate fromDate = currentInstallment.getFromDate();
LocalDate toDate = currentInstallment.getDueDate();
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
boolean prepayAttempt = ctx.isPrepayAttempt();
LoanInterestRecalculationDetails loanInterestRecalculationDetails = ctx.getInstallments().getFirst().getLoan()
.getLoanInterestRecalculationDetails();
if (!currentDate.equals(model.lastOverdueBalanceChange())) {
if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) {
emiCalculator.addBalanceCorrection(model, fromDate, overduePrincipal);
} else {
emiCalculator.addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
}
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
LocalDate lastOverdueBalanceChange;
if (shouldRecalculateTillInstallmentDueDate(loanInterestRecalculationDetails, prepayAttempt)) {
lastOverdueBalanceChange = toDate;
} else {
lastOverdueBalanceChange = currentDate;
}
emiCalculator.addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
model.lastOverdueBalanceChange(lastOverdueBalanceChange);
}
return true;
}
return false;
}
private void updateInstallmentsPrincipalAndInterestByModel(ProgressiveTransactionCtx ctx) {
ctx.getModel().repaymentPeriods().forEach(repayment -> {
LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream()
@@ -2991,9 +2916,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
private void cleanupInstallmentsAndRepaymentPeriodsAfterReAging(final ProgressiveTransactionCtx ctx) {
private void cleanupInstallmentsAfterReAging(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)
@@ -3001,17 +2925,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
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);
}
}
@@ -3050,7 +2967,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
});
cleanupInstallmentsAndRepaymentPeriodsAfterReAging(ctx);
cleanupInstallmentsAfterReAging(ctx);
}
private void reprocessInstallments(final List<LoanRepaymentScheduleInstallment> installments) {
@@ -3307,47 +3224,29 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
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 List<LoanRepaymentScheduleInstallment> installmentsBeforeReAging = installments.stream()
.filter(rp -> rp.getFromDate().isBefore(reAgingStartDate)).toList();
final RepaymentPeriod lastPeriod = periodsBeforeReAging.getLast();
// Define the date, which must match reAgingStartDate
final LocalDate expectedReAgingDate = installmentsBeforeReAging.isEmpty() ? installments.getFirst().getFromDate()
: installmentsBeforeReAging.getLast().getDueDate();
if (!lastPeriod.getDueDate().isEqual(reAgingStartDate)) {
if (!expectedReAgingDate.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 Money interestFromZeroedInstallments = emiCalculator.getOutstandingInterestTillDate(ctx.getModel(),
loanTransaction.getTransactionDate());
final BigDecimal interestRate = loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate();
final Money totalOutstandingPrincipal = ctx.getModel().getTotalOutstandingPrincipal();
final LoanApplicationTerms loanApplicationTerms = new LoanApplicationTerms.Builder().currency(currency.getCurrencyData())
.repaymentsStartingFromDate(reAgingStartDate).principal(outstandingPrincipalBalance.get())
.repaymentsStartingFromDate(reAgingStartDate).principal(totalOutstandingPrincipal)
.loanTermFrequency(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())
.loanTermPeriodFrequencyType(loanTransaction.getLoanReAgeParameter().getFrequencyType())
.numberOfRepayments(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())
@@ -3366,11 +3265,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.mc(mc).build();
// Update the existing model with re-aged periods
emiCalculator.updateModelRepaymentPeriodsDuringReAge(ctx.getModel(), loanTransaction, loanApplicationTerms, mc);
emiCalculator.updateModelRepaymentPeriodsDuringReAge(ctx, loanTransaction, loanApplicationTerms, mc);
updateInstallmentsByModelForReAging(loanTransaction, ctx);
loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(),
Money.of(currency, interestFromZeroedInstallments.get()), Money.zero(currency), Money.zero(currency));
loanTransaction.updateComponentsAndTotal(totalOutstandingPrincipal, interestFromZeroedInstallments, Money.zero(currency),
Money.zero(currency));
reprocessInstallments(installments);
}
}
@@ -28,6 +28,7 @@ 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.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
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;
@@ -143,6 +144,14 @@ public interface EMICalculator {
*/
void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate);
void updateModelRepaymentPeriodsDuringReAge(ProgressiveLoanInterestScheduleModel scheduleModel, LoanTransaction loanTransaction,
void updateModelRepaymentPeriodsDuringReAge(ProgressiveTransactionCtx ctx, LoanTransaction loanTransaction,
LoanApplicationTerms loanApplicationTerms, MathContext mc);
boolean recalculateModelOverdueAmountsTillDate(ProgressiveTransactionCtx ctx, LocalDate targetDate);
/**
* Gives back the sum of the outstanding interest from the whole model till the provided date.
*/
@NotNull
Money getOutstandingInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate tillDate);
}
@@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
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.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
@@ -46,10 +47,13 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
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.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
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;
@@ -571,20 +575,126 @@ 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 List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods();
public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveTransactionCtx ctx, final LoanTransaction loanTransaction,
final LoanApplicationTerms loanApplicationTerms, final MathContext mc) {
final List<RepaymentPeriod> existingRepaymentPeriods = ctx.getModel().repaymentPeriods();
moveOutstandingAmountsFromPeriodsBeforeReAging(existingRepaymentPeriods, reAgingStartDate);
moveOutstandingAmountsFromPeriodsBeforeReAging(existingRepaymentPeriods, loanTransaction.getLoanReAgeParameter().getStartDate());
final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate(loanReAgeParameter);
final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate(loanTransaction);
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryReAgedScheduleModel(loanApplicationTerms,
mc, periodStartDate);
mergeNewInterestScheduleModelWithExistingOne(scheduleModel, temporaryReAgedScheduleModel, loanTransaction);
mergeNewInterestScheduleModelWithExistingOne(ctx, temporaryReAgedScheduleModel, loanTransaction);
}
@Override
public boolean recalculateModelOverdueAmountsTillDate(final ProgressiveTransactionCtx ctx, final LocalDate targetDate) {
boolean hasChange = false;
final ProgressiveLoanInterestScheduleModel model = ctx.getModel();
final List<RepaymentPeriod> overdueInstallmentsSortedByInstallmentNumber = findPossiblyOverdueRepaymentPeriods(targetDate, model);
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
final RepaymentPeriod lastPeriod = model.getLastRepaymentPeriod();
final RepaymentPeriod currentPeriod = model.findRepaymentPeriod(targetDate).orElse(lastPeriod);
final MonetaryCurrency currency = model.zero().getCurrency();
Money overDuePrincipal = Money.zero(currency);
Money aggregatedOverDuePrincipal = Money.zero(currency);
for (RepaymentPeriod processingPeriod : overdueInstallmentsSortedByInstallmentNumber) {
// add and subtract outstanding principal
if (!overDuePrincipal.isZero()) {
final boolean currentChanges = adjustOverduePrincipal(targetDate, processingPeriod, overDuePrincipal,
aggregatedOverDuePrincipal, ctx);
hasChange = hasChange || currentChanges;
}
overDuePrincipal = processingPeriod.getOutstandingPrincipal();
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
}
if (!currentPeriod.equals(lastPeriod) || !targetDate.isAfter(lastPeriod.getDueDate())) {
final boolean currentChanges = adjustOverduePrincipal(targetDate, currentPeriod, overDuePrincipal,
aggregatedOverDuePrincipal, ctx);
hasChange = hasChange || currentChanges;
}
if (aggregatedOverDuePrincipal.isGreaterThanZero()
&& (model.lastOverdueBalanceChange() == null || model.lastOverdueBalanceChange().isBefore(targetDate))) {
model.lastOverdueBalanceChange(targetDate);
}
}
return hasChange;
}
@Override
public Money getOutstandingInterestTillDate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate tillDate) {
final ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
tillDate);
return recalculatedScheduleModelTillDate.repaymentPeriods().stream()
.filter(repaymentPeriod -> repaymentPeriod.getFromDate().isBefore(tillDate)).map(RepaymentPeriod::getOutstandingInterest)
.reduce(scheduleModel.zero(), Money::add);
}
private List<RepaymentPeriod> findPossiblyOverdueRepaymentPeriods(final LocalDate targetDate,
final ProgressiveLoanInterestScheduleModel model) {
return model.repaymentPeriods().stream() //
.filter(repaymentPeriod -> DateUtils.isAfter(targetDate, repaymentPeriod.getDueDate())).toList();
}
private boolean adjustOverduePrincipal(final LocalDate currentDate, final RepaymentPeriod currentInstallment,
final Money overduePrincipal, final Money aggregatedOverDuePrincipal, final ProgressiveTransactionCtx ctx) {
final LocalDate fromDate = currentInstallment.getFromDate();
final LocalDate toDate = currentInstallment.getDueDate();
final ProgressiveLoanInterestScheduleModel model = ctx.getModel();
final boolean prepayAttempt = ctx.isPrepayAttempt();
final LoanInterestRecalculationDetails loanInterestRecalculationDetails = ctx.getInstallments().getFirst().getLoan()
.getLoanInterestRecalculationDetails();
if (!currentDate.equals(model.lastOverdueBalanceChange())) {
if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) {
addBalanceCorrection(model, fromDate, overduePrincipal);
} else {
addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
}
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
LocalDate lastOverdueBalanceChange;
if (shouldRecalculateTillInstallmentDueDate(loanInterestRecalculationDetails, prepayAttempt)) {
lastOverdueBalanceChange = toDate;
} else {
lastOverdueBalanceChange = currentDate;
}
addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
model.lastOverdueBalanceChange(lastOverdueBalanceChange);
}
return true;
}
return false;
}
private boolean shouldRecalculateTillInstallmentDueDate(final LoanInterestRecalculationDetails recalculationDetails,
final boolean isPrepayAttempt) {
// Rest frequency type and pre close interest calculation strategy can be controversial
// if restFrequencyType == DAILY and preCloseInterestCalculationStrategy == TILL_PRE_CLOSURE_DATE
// no problem. Calculate till transaction date
// if restFrequencyType == SAME_AS_REPAYMENT_PERIOD and preCloseInterestCalculationStrategy ==
// TILL_REST_FREQUENCY_DATE
// again, no problem. Calculate till due date of current installment
// if restFrequencyType == DAILY and preCloseInterestCalculationStrategy == TILL_REST_FREQUENCY_DATE
// or restFrequencyType == SAME_AS_REPAYMENT_PERIOD and preCloseInterestCalculationStrategy ==
// TILL_PRE_CLOSURE_DATE
// we cannot harmonize the two configs. Behaviour should mimic prepay api.
return switch (recalculationDetails.getRestFrequencyType()) {
case DAILY ->
isPrepayAttempt && recalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled();
case SAME_AS_REPAYMENT_PERIOD ->
recalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled();
case WEEKLY -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WEEKLY");
case MONTHLY -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: MONTHLY");
case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID");
};
}
/**
@@ -592,18 +702,23 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* 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,
private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveTransactionCtx ctx,
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel, final LoanTransaction loanTransaction) {
final List<RepaymentPeriod> newPeriods = temporaryReAgedScheduleModel.repaymentPeriods();
final ProgressiveLoanInterestScheduleModel scheduleModel = ctx.getModel();
final Loan loan = loanTransaction.getLoan();
if (newPeriods.isEmpty()) {
return;
}
final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods();
final RepaymentPeriod firstRepaymentPeriod = existingRepaymentPeriods.getFirst();
final Money disbursedAmount = firstRepaymentPeriod.getTotalDisbursedAmount();
final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate();
final Optional<RepaymentPeriod> firstExistingRepaymentPeriodOpt = existingRepaymentPeriods.stream()
final Optional<RepaymentPeriod> firstExistingReAgedRepaymentPeriodOpt = existingRepaymentPeriods.stream()
.filter(period -> period.getDueDate().equals(reAgingStartDate)).findFirst();
for (final RepaymentPeriod newPeriod : newPeriods) {
@@ -611,8 +726,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
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())) {
if (existingRepaymentPeriodOpt.isPresent() && firstExistingReAgedRepaymentPeriodOpt.isPresent()
&& existingRepaymentPeriodOpt.get().equals(firstExistingReAgedRepaymentPeriodOpt.get())) {
previousExistingRepaymentPeriodOpt = existingRepaymentPeriodOpt.get().getPrevious();
}
@@ -623,25 +738,53 @@ public final class ProgressiveEMICalculator implements EMICalculator {
previousExistingRepaymentPeriodOpt.orElseGet(existingRepaymentPeriods::getLast), newPeriod.getFromDate(),
newPeriod.getDueDate(), newPrincipal.add(newInterest), MoneyHelper.getMathContext(),
loanTransaction.getLoan().getLoanProductRelatedDetail());
rp.setTotalDisbursedAmount(scheduleModel.repaymentPeriods().getFirst().getTotalDisbursedAmount());
rp.setTotalDisbursedAmount(disbursedAmount);
if (existingRepaymentPeriodOpt.isPresent()) {
// Add an interest period on the disbursement date for the very first replaced period
if (existingRepaymentPeriodOpt.get().equals(firstRepaymentPeriod)) {
final LocalDate disbursementDate = loanTransaction.getLoan().getDisbursementDate();
rp.getInterestPeriods().getFirst().setDisbursementAmount(disbursedAmount);
rp.getInterestPeriods().getFirst().setFromDate(disbursementDate);
rp.getInterestPeriods().getFirst().setDueDate(disbursementDate);
rp.getInterestPeriods().add(InterestPeriod.withEmptyAmounts(rp, rp.getFromDate(), rp.getDueDate()));
}
existingRepaymentPeriods.remove(existingRepaymentPeriodOpt.get());
}
existingRepaymentPeriodOpt.ifPresent(existingRepaymentPeriods::remove);
existingRepaymentPeriods.add(rp);
calculateRateFactorForRepaymentPeriod(rp, scheduleModel);
}
final RepaymentPeriod lastReAgedInstallment = newPeriods.getLast();
if (reAgingStartDate.isBefore(DateUtils.getBusinessLocalDate()) && isInterestRecalculationIsAllowed(ctx, loan)) {
scheduleModel.lastOverdueBalanceChange(null);
recalculateModelOverdueAmountsTillDate(ctx, loanTransaction.getSubmittedOnDate());
}
final List<RepaymentPeriod> reAgedRepaymentPeriods = existingRepaymentPeriods.stream()
.filter(repaymentPeriod -> (!repaymentPeriod.getFromDate().isBefore(reAgingStartDate)
|| repaymentPeriod.getDueDate().isEqual(reAgingStartDate))
&& !repaymentPeriod.getDueDate().isAfter(lastReAgedInstallment.getDueDate()))
&& !repaymentPeriod.getDueDate().isAfter(newPeriods.getLast().getDueDate()))
.toList();
// cleanup repayment periods after re-aging
if (reAgedRepaymentPeriods.getLast() != null) {
final List<RepaymentPeriod> repaymentPeriodsToRemove = existingRepaymentPeriods.stream()
.filter(rp -> !rp.getFromDate().isBefore(reAgedRepaymentPeriods.getLast().getDueDate())).toList();
repaymentPeriodsToRemove.forEach(existingRepaymentPeriods::remove);
}
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, loanTransaction.getTransactionDate());
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, reAgedRepaymentPeriods);
}
private boolean isInterestRecalculationIsAllowed(final ProgressiveTransactionCtx ctx, final Loan loan) {
return loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff() && !ctx.isWrittenOff()
&& !ctx.isContractTerminated() && !loan.isNpa()
&& !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue();
}
/**
* * Generates temporary interestScheduleModel with re-aged repayment periods
*/
@@ -663,8 +806,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* is used to generate re-aged repayment periods
*/
@NotNull
private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanReAgeParameter loanReAgeParameter) {
private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanTransaction loanTransaction) {
final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
if (reAgingStartDate.isEqual(loanTransaction.getLoan().getDisbursementDate())) {
return reAgingStartDate;
}
return switch (loanReAgeParameter.getFrequencyType()) {
case DAYS -> reAgingStartDate.minusDays(loanReAgeParameter.getFrequencyNumber());
case WEEKS -> reAgingStartDate.minusWeeks(loanReAgeParameter.getFrequencyNumber());
@@ -60,6 +60,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
private Money creditedPrincipal;
/** Stores credited interest. Related transaction: Chargeback */
private Money creditedInterest;
@Setter
private Money disbursementAmount;
private Money balanceCorrectionAmount;
private Money outstandingLoanBalance;