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 d2497a77fcedf51f231d2603ea77987add459143
Author: Soma Sörös <[email protected]>
AuthorDate: Thu Jan 29 12:06:33 2026 +0100

    FINERACT-2412: full term tranche - N+1 installment handling
---
 ...dvancedPaymentScheduleTransactionProcessor.java | 44 +++++++++++++++++-----
 .../impl/ProgressiveTransactionCtx.java            | 29 ++++++++++++++
 2 files changed, 64 insertions(+), 9 deletions(-)

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 a058441095..ea0aeedb42 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
@@ -44,6 +44,7 @@ import java.util.Comparator;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -228,12 +229,13 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
         final Integer installmentAmountInMultiplesOf = 
loan.getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf();
         ProgressiveLoanInterestScheduleModel scheduleModel = 
emiCalculator.generateInstallmentInterestScheduleModel(installments,
                 LoanConfigurationDetailsMapper.map(loan), 
installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc());
+        List<Long> loanChargeIdProcessed = new ArrayList<>();
+
         ProgressiveTransactionCtx ctx = new 
ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
-                changedTransactionDetail, scheduleModel, 
loan.getActiveLoanTermVariations());
+                changedTransactionDetail, scheduleModel, Money.zero(currency), 
loan.getActiveLoanTermVariations(), loanChargeIdProcessed);
 
         List<ChangeOperation> changeOperations = 
createSortedChangeList(loanTermVariations, loanTransactions, charges);
 
-        List<Long> loanChargeIdProcessed = new ArrayList<>();
         List<LoanTransaction> overpaidTransactions = new ArrayList<>();
         for (final ChangeOperation changeOperation : changeOperations) {
             if (changeOperation.isLoanTermVariationsData()) {
@@ -1595,7 +1597,8 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
         }
 
         disbursementTransaction.resetDerivedComponents();
-        recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, 
model, installments, disbursementTransaction, currency);
+        recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, 
model, installments, disbursementTransaction, currency,
+                ((ProgressiveTransactionCtx) 
transactionCtx).getProcessedLoanCharges());
         allocateOverpayment(disbursementTransaction, transactionCtx);
     }
 
@@ -1668,7 +1671,8 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
         Money amortizableAmount = 
capitalizedIncomeTransaction.getAmount(currency);
         emiCalculator.addCapitalizedIncome(model, transactionDate, 
amortizableAmount);
 
-        recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, 
model, installments, capitalizedIncomeTransaction, currency);
+        recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, 
model, installments, capitalizedIncomeTransaction, currency,
+                ((ProgressiveTransactionCtx) 
transactionCtx).getProcessedLoanCharges());
         allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
     }
 
@@ -1690,7 +1694,7 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
 
     private void recalculateRepaymentPeriodsWithEMICalculation(final Money 
amortizableAmount,
             final ProgressiveLoanInterestScheduleModel model, final 
List<LoanRepaymentScheduleInstallment> installments,
-            final LoanTransaction loanTransaction, final MonetaryCurrency 
currency) {
+            final LoanTransaction loanTransaction, final MonetaryCurrency 
currency, final Set<LoanCharge> processedLoanCharges) {
         final LocalDate transactionDate = loanTransaction.getTransactionDate();
         final boolean isPostMaturityDisbursement = 
installments.stream().filter(i -> !i.isDownPayment() && !i.isAdditional())
                 .allMatch(i -> i.getDueDate().isBefore(transactionDate));
@@ -1708,6 +1712,8 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
             final AtomicInteger installmentCounter = new AtomicInteger();
             final ILoanConfigurationDetails loanProductRelatedDetail = 
model.loanProductRelatedDetail();
 
+            List<LoanRepaymentScheduleInstallment> newInstallments = new 
LinkedList<>();
+
             model.repaymentPeriods().forEach(rm -> {
                 LoanRepaymentScheduleInstallment installment = null;
                 while (iterator.hasNext() && (installment == null || 
installment.isAdditional() || installment.isDownPayment())) {
@@ -1723,7 +1729,9 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
                 } else {
                     if (loanProductRelatedDetail != null && 
loanProductRelatedDetail.isAllowFullTermForTranche()
                             && 
loanProductRelatedDetail.getNumberOfRepayments() > 0 && 
!rm.getDueDate().isBefore(transactionDate)) {
-                        installmentCounter.getAndIncrement();
+                        if (installment == null || 
!installment.isAdditional()) {
+                            installmentCounter.getAndIncrement();
+                        }
                         final LoanRepaymentScheduleInstallment newInstallment 
= new LoanRepaymentScheduleInstallment(
                                 loanTransaction.getLoan(), 
installmentCounter.get(), rm.getFromDate(), rm.getDueDate(),
                                 rm.getDuePrincipal().getAmount(), 
rm.getDueInterest().getAmount(), null, null, null, null, null, null,
@@ -1731,9 +1739,27 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
 
                         newInstallment.updateObligationsMet(currency, 
transactionDate);
                         iterator.add(newInstallment);
+                        newInstallments.add(newInstallment);
                     }
                 }
             });
+            // fix additional installment
+            Optional<LoanRepaymentScheduleInstallment> 
additionalInstallmentOptional = installments.stream()
+                    
.filter(LoanRepaymentScheduleInstallment::isAdditional).findFirst();
+            if (additionalInstallmentOptional.isPresent() && 
!newInstallments.isEmpty()) {
+                LoanRepaymentScheduleInstallment additional = 
additionalInstallmentOptional.get();
+                // iterate trough new installments to fix charges
+                for (LoanRepaymentScheduleInstallment installment : 
newInstallments) {
+                    moveRelatedChargesToInstallment(processedLoanCharges, 
installment, List.of(additional), currency);
+                    additional.setFromDate(installment.getDueDate());
+                    
additional.setInstallmentNumber(installment.getInstallmentNumber() + 1);
+                }
+                installments.remove(additional);
+                if (additional.getDueDate().isAfter(model.getMaturityDate())) {
+                    // step is needed to move the additional installment to 
the end of the list.
+                    installments.add(additional);
+                }
+            }
         }
     }
 
@@ -3866,11 +3892,11 @@ public class 
AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
     }
 
     private void moveRelatedChargesToInstallment(Set<LoanCharge> charges, 
LoanRepaymentScheduleInstallment target,
-            List<LoanRepaymentScheduleInstallment> installments, 
MonetaryCurrency currency) {
-        int firstNormalInstallmentNumber = 
LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
+            List<LoanRepaymentScheduleInstallment> sources, MonetaryCurrency 
currency) {
+        int firstNormalInstallmentNumber = 
LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(sources);
         Set<LoanCharge> chargesOfNewInstallment = 
getLoanChargesOfInstallment(charges, target, firstNormalInstallmentNumber);
         Integer targetInstallmentNumber = target.getInstallmentNumber();
-        installments.stream().filter(i -> 
Objects.equals(i.getInstallmentNumber(), targetInstallmentNumber)).findFirst()
+        sources.stream().filter(source -> 
Objects.equals(source.getInstallmentNumber(), 
targetInstallmentNumber)).findFirst()
                 .filter(source -> source != target).ifPresent(source -> {
                     // move fees
                     
chargesOfNewInstallment.stream().filter(LoanCharge::isNotFullyPaid).filter(LoanCharge::isFeeCharge)
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java
index a0bff2228a..2dc3a87c81 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java
@@ -19,10 +19,13 @@
 package 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 import lombok.Getter;
 import lombok.Setter;
+import 
org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
 import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
 import org.apache.fineract.organisation.monetary.domain.Money;
 import 
org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
@@ -50,6 +53,7 @@ public class ProgressiveTransactionCtx extends TransactionCtx 
{
     @Setter
     private boolean isPrepayAttempt = false;
     private final List<LoanRepaymentScheduleInstallment> 
skipRepaymentScheduleInstallments = new ArrayList<>();
+    private final List<Long> processedLoanChargeIds;
 
     public ProgressiveTransactionCtx(MonetaryCurrency currency, 
List<LoanRepaymentScheduleInstallment> installments,
             Set<LoanCharge> charges, MoneyHolder overpaymentHolder, 
ChangedTransactionDetail changedTransactionDetail,
@@ -62,8 +66,33 @@ public class ProgressiveTransactionCtx extends 
TransactionCtx {
             Set<LoanCharge> charges, MoneyHolder overpaymentHolder, 
ChangedTransactionDetail changedTransactionDetail,
             ProgressiveLoanInterestScheduleModel model, Money 
sumOfInterestRefundAmount,
             List<LoanTermVariations> activeLoanTermVariations) {
+        this(currency, installments, charges, overpaymentHolder, 
changedTransactionDetail, model, sumOfInterestRefundAmount,
+                activeLoanTermVariations,
+                charges == null ? new ArrayList<>() : 
charges.stream().map(AbstractPersistableCustom::getId).toList());
+    }
+
+    public ProgressiveTransactionCtx(MonetaryCurrency currency, 
List<LoanRepaymentScheduleInstallment> installments,
+            Set<LoanCharge> charges, MoneyHolder overpaymentHolder, 
ChangedTransactionDetail changedTransactionDetail,
+            ProgressiveLoanInterestScheduleModel model, Money 
sumOfInterestRefundAmount, List<LoanTermVariations> activeLoanTermVariations,
+            List<Long> processedLoanChargeIds) {
         super(currency, installments, charges, overpaymentHolder, 
changedTransactionDetail, activeLoanTermVariations);
         this.sumOfInterestRefundAmount = sumOfInterestRefundAmount;
         this.model = model;
+        this.processedLoanChargeIds = processedLoanChargeIds;
     }
+
+    public Set<LoanCharge> getProcessedLoanCharges() {
+        if (getCharges() == null) {
+            return new HashSet<>();
+        }
+        if (getCharges().size() == getProcessedLoanChargeIds().size()) {
+            return getCharges();
+        }
+        return 
getCharges().stream().filter(this::isLoanChargeProcessed).collect(Collectors.toSet());
+    }
+
+    public boolean isLoanChargeProcessed(final LoanCharge loanCharge) {
+        return getProcessedLoanChargeIds().contains(loanCharge.getId());
+    }
+
 }

Reply via email to