FINERACT-2221: Fix - Interest not counted towards totalUnpaidPayableNotDueInterest after partial repayment

This commit is contained in:
Adam Saghy
2025-03-24 14:23:00 +01:00
parent 2251ee0da6
commit b2af4a635c
11 changed files with 261 additions and 46 deletions
@@ -87,13 +87,13 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr
totalRepaymentTransactionReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances,
LoanTransactionType.REPAYMENT);
if (repaymentSchedule != null) {
if (repaymentSchedule != null && defaultSummaryData.getInterestCharged().compareTo(BigDecimal.ZERO) > 0) {
// Outstanding Interest on Past due installments
totalUnpaidPayableDueInterest = computeTotalUnpaidPayableDueInterestAmount(repaymentSchedule.getPeriods(), businessDate);
// Accumulated daily interest of the current Installment period
totalUnpaidPayableNotDueInterest = computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(loan,
repaymentSchedule.getPeriods(), businessDate, defaultSummaryData.getCurrency());
repaymentSchedule.getPeriods(), businessDate, defaultSummaryData.getCurrency(), totalUnpaidPayableDueInterest);
}
return LoanSummaryData.builder().currency(defaultSummaryData.getCurrency())
@@ -44,7 +44,8 @@ public class CumulativeLoanSummaryDataProvider extends CommonLoanSummaryDataProv
@Override
public BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(final Loan loan,
final Collection<LoanSchedulePeriodData> periods, final LocalDate businessDate, final CurrencyData currency) {
final Collection<LoanSchedulePeriodData> periods, final LocalDate businessDate, final CurrencyData currency,
BigDecimal totalUnpaidPayableDueInterest) {
// Find the current Period (If exists one) based on the Business date
final Optional<LoanSchedulePeriodData> optCurrentPeriod = periods.stream().filter(period -> !period.isDownPaymentPeriod() //
&& period.getPeriod() != null //
@@ -33,7 +33,7 @@ public interface LoanSummaryDataProvider {
BigDecimal computeTotalUnpaidPayableDueInterestAmount(Collection<LoanSchedulePeriodData> periods, LocalDate businessDate);
BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(Loan loan, Collection<LoanSchedulePeriodData> periods,
LocalDate businessDate, CurrencyData currency);
LocalDate businessDate, CurrencyData currency, BigDecimal totalUnpaidPayableDueInterest);
LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData);
@@ -21,9 +21,9 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
@@ -40,7 +40,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.springframework.stereotype.Component;
@@ -71,26 +71,25 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances);
}
private LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(Loan loan, LocalDate businessDate) {
private Optional<LoanRepaymentScheduleInstallment> getRelatedRepaymentScheduleInstallment(Loan loan, LocalDate businessDate) {
return loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isDownPayment() && !i.isAdditional()
&& !businessDate.isBefore(i.getFromDate()) && businessDate.isBefore(i.getDueDate())).findFirst().orElseGet(() -> {
List<LoanRepaymentScheduleInstallment> list = loan.getRepaymentScheduleInstallments().stream()
.filter(i -> !i.isDownPayment() && !i.isAdditional()).toList();
return !list.isEmpty() ? list.get(list.size() - 1) : null;
});
&& businessDate.isAfter(i.getFromDate()) && !businessDate.isAfter(i.getDueDate())).findFirst();
}
@Override
public BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(final Loan loan,
final Collection<LoanSchedulePeriodData> periods, final LocalDate businessDate, final CurrencyData currency) {
if (loan.isMatured(businessDate)) {
final Collection<LoanSchedulePeriodData> periods, final LocalDate businessDate, final CurrencyData currency,
BigDecimal totalUnpaidPayableDueInterest) {
if (loan.isMatured(businessDate) || !loan.isInterestBearing()) {
return BigDecimal.ZERO;
}
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = getRelatedRepaymentScheduleInstallment(loan, businessDate);
if (loan.isInterestBearing() && loanRepaymentScheduleInstallment != null) {
Optional<LoanRepaymentScheduleInstallment> currentRepaymentPeriod = getRelatedRepaymentScheduleInstallment(loan, businessDate);
if (currentRepaymentPeriod.isPresent()) {
if (loan.isChargedOff()) {
return loanRepaymentScheduleInstallment.getInterestOutstanding(loan.getCurrency()).getAmount();
return MathUtil.subtractToZero(currentRepaymentPeriod.get().getInterestOutstanding(loan.getCurrency()).getAmount(),
totalUnpaidPayableDueInterest);
} else {
List<LoanTransaction> transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream()
.filter(t -> !t.isAccrualActivity()).toList();
@@ -107,20 +106,15 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
replayedTransactions);
}
if (model != null) {
LoanRepaymentScheduleInstallment nextUnpaidInAdvanceInstallment = loanRepaymentScheduleInstallment.isNotFullyPaidOff()
? loanRepaymentScheduleInstallment
: loan.getRepaymentScheduleInstallments().stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(i -> i.getInstallmentNumber() != null)
.min(Comparator.comparingInt(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
if (nextUnpaidInAdvanceInstallment == null) {
return BigDecimal.ZERO;
}
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(model, nextUnpaidInAdvanceInstallment.getDueDate(),
businessDate);
if (dueAmounts != null) {
BigDecimal interestPaid = nextUnpaidInAdvanceInstallment.getInterestPaid();
BigDecimal dueInterest = dueAmounts.getDueInterest().getAmount();
return MathUtil.subtractToZero(dueInterest, interestPaid);
OutstandingDetails outstandingDetails = emiCalculator.getOutstandingAmountsTillDate(model, businessDate);
if (!loan.isInterestRecalculationEnabled()) {
BigDecimal interestPaid = periods.stream().map(LoanSchedulePeriodData::getInterestPaid).reduce(BigDecimal.ZERO,
BigDecimal::add);
BigDecimal dueInterest = outstandingDetails.getOutstandingInterest().getAmount();
return MathUtil.subtractToZero(dueInterest, interestPaid, totalUnpaidPayableDueInterest);
} else {
return MathUtil.subtractToZero(outstandingDetails.getOutstandingInterest().getAmount(),
totalUnpaidPayableDueInterest);
}
}
}