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()); + } + }
