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) 
{

Reply via email to