This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit d093a089c9c13051eb8922100e38e44a3933fd9c Author: Oleksii Novikov <[email protected]> AuthorDate: Tue Jan 13 14:12:30 2026 +0200 FINERACT-2354: Fix charge-off after re-age --- .../domain/LoanRepaymentScheduleInstallment.java | 2 +- ...dvancedPaymentScheduleTransactionProcessor.java | 42 ++++++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 1330394cfb..d66c6bb2c6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -1147,7 +1147,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa } public void copyFrom(final LoanRepaymentScheduleInstallment installment) { - if (nonNullAndEqual(getId(), installment.getId())) { + if (installment == this || nonNullAndEqual(getId(), installment.getId())) { return; } // Reset balances diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 018bce06a2..0bb8f42fe6 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -2025,14 +2025,20 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep final LoanRepaymentScheduleInstallment currentInstallment = loan.getRelatedRepaymentScheduleInstallment(transactionDate); if (!installments.isEmpty() && transactionDate.isBefore(loan.getMaturityDate()) && currentInstallment != null) { - if (currentInstallment.isNotFullyPaidOff()) { + if (currentInstallment.isNotFullyPaidOff() || currentInstallment.isReAged()) { if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx && loan.isInterestBearingAndInterestRecalculationEnabled()) { final BigDecimal interestOutstanding = currentInstallment.getInterestOutstanding(loan.getCurrency()).getAmount(); final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(), currentInstallment.getFromDate(), currentInstallment.getDueDate(), transactionDate, true, false).getAmount(); - if (interestOutstanding.compareTo(BigDecimal.ZERO) > 0 || newInterest.compareTo(BigDecimal.ZERO) > 0) { - currentInstallment.updateInterestCharged(newInterest); + // Collect fixed interest from future re-aged periods that will be removed + final BigDecimal futureFixedInterest = progressiveTransactionCtx.getModel().repaymentPeriods().stream() + .filter(rp -> DateUtils.isAfterInclusive(rp.getFromDate(), transactionDate)).filter(RepaymentPeriod::isReAged) + .filter(rp -> !rp.getFixedInterest().isZero()).map(rp -> rp.getFixedInterest().getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + final BigDecimal totalInterest = newInterest.add(futureFixedInterest); + if (interestOutstanding.compareTo(BigDecimal.ZERO) > 0 || totalInterest.compareTo(BigDecimal.ZERO) > 0) { + currentInstallment.updateInterestCharged(totalInterest); } } else { final BigDecimal totalInterest = currentInstallment.getInterestOutstanding(transactionCtx.getCurrency()).getAmount(); @@ -2157,9 +2163,20 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep calculatePartialPeriodInterest(transactionCtx, transactionDate); } + // Check if re-aging (before charge-off) used equal amortization - in that case, preserve interest for + // re-aged installments + final boolean reAgingUsedEqualAmortization = loanTransaction.getLoan().getLoanTransactions().stream() // + .filter(LoanTransaction::isReAge) // + .filter(t -> !t.getTransactionDate().isAfter(transactionDate)) // + .map(LoanTransaction::getLoanReAgeParameter) // + .filter(Objects::nonNull) // + .map(LoanReAgeParameter::getInterestHandlingType) // + .anyMatch(type -> type == LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST + || type == LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST); + installments.stream() .filter(installment -> installment.getFromDate().isAfter(transactionDate) && !installment.isObligationsMet()) - .forEach(installment -> { + .filter(installment -> !(installment.isReAged() && reAgingUsedEqualAmortization)).forEach(installment -> { final BigDecimal interestOutstanding = installment.getInterestOutstanding(currency).getAmount(); final BigDecimal updatedInterestCharged = installment.getInterestCharged(currency).getAmount() .subtract(interestOutstanding); @@ -3381,17 +3398,28 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep lastPeriod.setDueDate(transactionDate); lastPeriod.getInterestPeriods().removeIf(interestPeriod -> !interestPeriod.getFromDate().isBefore(transactionDate)); - transactionCtx.getModel().repaymentPeriods().removeAll(periodsToRemove); - final BigDecimal totalPrincipal = periodsToRemove.stream().map(rp -> rp.getDuePrincipal().getAmount()).reduce(BigDecimal.ZERO, BigDecimal::add); + final BigDecimal futureInterest = periodsToRemove.stream().filter(RepaymentPeriod::isReAged) + .filter(rp -> !rp.getFixedInterest().isZero()).map(rp -> rp.getFixedInterest().getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + transactionCtx.getModel().repaymentPeriods().removeAll(periodsToRemove); + final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(transactionCtx.getModel(), lastPeriod.getFromDate(), lastPeriod.getDueDate(), transactionDate, false, false).getAmount(); - lastPeriod.setEmi(lastPeriod.getDuePrincipal().add(totalPrincipal).add(newInterest)); + if (futureInterest.compareTo(BigDecimal.ZERO) > 0) { + final MonetaryCurrency currency = transactionCtx.getCurrency(); + final MathContext mc = transactionCtx.getModel().mc(); + lastPeriod.setFixedInterest(lastPeriod.getFixedInterest().add(Money.of(currency, futureInterest, mc), mc)); + } + + lastPeriod.setEmi(lastPeriod.getDuePrincipal().add(totalPrincipal).add(newInterest).add(futureInterest)); emiCalculator.calculateRateFactorForRepaymentPeriod(lastPeriod, transactionCtx.getModel()); + transactionCtx.getModel().disableEMIRecalculation(); for (LoanTransaction processTransaction : transactionsToBeReprocessed) {
