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 a05844109..ea0aeedb4 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 loanChargeIdProcessed = new ArrayList<>(); + ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, - changedTransactionDetail, scheduleModel, loan.getActiveLoanTermVariations()); + changedTransactionDetail, scheduleModel, Money.zero(currency), loan.getActiveLoanTermVariations(), loanChargeIdProcessed); List changeOperations = createSortedChangeList(loanTermVariations, loanTransactions, charges); - List loanChargeIdProcessed = new ArrayList<>(); List 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 installments, - final LoanTransaction loanTransaction, final MonetaryCurrency currency) { + final LoanTransaction loanTransaction, final MonetaryCurrency currency, final Set 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 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 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 charges, LoanRepaymentScheduleInstallment target, - List installments, MonetaryCurrency currency) { - int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); + List sources, MonetaryCurrency currency) { + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(sources); Set 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 a0bff2228..2dc3a87c8 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 skipRepaymentScheduleInstallments = new ArrayList<>(); + private final List processedLoanChargeIds; public ProgressiveTransactionCtx(MonetaryCurrency currency, List installments, Set charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail, @@ -62,8 +66,33 @@ public class ProgressiveTransactionCtx extends TransactionCtx { Set charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail, ProgressiveLoanInterestScheduleModel model, Money sumOfInterestRefundAmount, List 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 installments, + Set charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail, + ProgressiveLoanInterestScheduleModel model, Money sumOfInterestRefundAmount, List activeLoanTermVariations, + List processedLoanChargeIds) { super(currency, installments, charges, overpaymentHolder, changedTransactionDetail, activeLoanTermVariations); this.sumOfInterestRefundAmount = sumOfInterestRefundAmount; this.model = model; + this.processedLoanChargeIds = processedLoanChargeIds; } + + public Set 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()); + } + }