FINERACT-2412: full term tranche - N+1 installment handling
This commit is contained in:
+35
-9
@@ -44,6 +44,7 @@ import java.util.Comparator;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ListIterator;
|
import java.util.ListIterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -228,12 +229,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
final Integer installmentAmountInMultiplesOf = loan.getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf();
|
final Integer installmentAmountInMultiplesOf = loan.getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf();
|
||||||
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments,
|
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments,
|
||||||
LoanConfigurationDetailsMapper.map(loan), installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc());
|
LoanConfigurationDetailsMapper.map(loan), installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc());
|
||||||
|
List<Long> loanChargeIdProcessed = new ArrayList<>();
|
||||||
|
|
||||||
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
|
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<ChangeOperation> changeOperations = createSortedChangeList(loanTermVariations, loanTransactions, charges);
|
||||||
|
|
||||||
List<Long> loanChargeIdProcessed = new ArrayList<>();
|
|
||||||
List<LoanTransaction> overpaidTransactions = new ArrayList<>();
|
List<LoanTransaction> overpaidTransactions = new ArrayList<>();
|
||||||
for (final ChangeOperation changeOperation : changeOperations) {
|
for (final ChangeOperation changeOperation : changeOperations) {
|
||||||
if (changeOperation.isLoanTermVariationsData()) {
|
if (changeOperation.isLoanTermVariationsData()) {
|
||||||
@@ -1595,7 +1597,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
|
|
||||||
disbursementTransaction.resetDerivedComponents();
|
disbursementTransaction.resetDerivedComponents();
|
||||||
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, disbursementTransaction, currency);
|
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, disbursementTransaction, currency,
|
||||||
|
((ProgressiveTransactionCtx) transactionCtx).getProcessedLoanCharges());
|
||||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1668,7 +1671,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency);
|
Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency);
|
||||||
emiCalculator.addCapitalizedIncome(model, transactionDate, amortizableAmount);
|
emiCalculator.addCapitalizedIncome(model, transactionDate, amortizableAmount);
|
||||||
|
|
||||||
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, capitalizedIncomeTransaction, currency);
|
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, capitalizedIncomeTransaction, currency,
|
||||||
|
((ProgressiveTransactionCtx) transactionCtx).getProcessedLoanCharges());
|
||||||
allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
|
allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1690,7 +1694,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
|
|
||||||
private void recalculateRepaymentPeriodsWithEMICalculation(final Money amortizableAmount,
|
private void recalculateRepaymentPeriodsWithEMICalculation(final Money amortizableAmount,
|
||||||
final ProgressiveLoanInterestScheduleModel model, final List<LoanRepaymentScheduleInstallment> installments,
|
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 LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||||
final boolean isPostMaturityDisbursement = installments.stream().filter(i -> !i.isDownPayment() && !i.isAdditional())
|
final boolean isPostMaturityDisbursement = installments.stream().filter(i -> !i.isDownPayment() && !i.isAdditional())
|
||||||
.allMatch(i -> i.getDueDate().isBefore(transactionDate));
|
.allMatch(i -> i.getDueDate().isBefore(transactionDate));
|
||||||
@@ -1708,6 +1712,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
final AtomicInteger installmentCounter = new AtomicInteger();
|
final AtomicInteger installmentCounter = new AtomicInteger();
|
||||||
final ILoanConfigurationDetails loanProductRelatedDetail = model.loanProductRelatedDetail();
|
final ILoanConfigurationDetails loanProductRelatedDetail = model.loanProductRelatedDetail();
|
||||||
|
|
||||||
|
List<LoanRepaymentScheduleInstallment> newInstallments = new LinkedList<>();
|
||||||
|
|
||||||
model.repaymentPeriods().forEach(rm -> {
|
model.repaymentPeriods().forEach(rm -> {
|
||||||
LoanRepaymentScheduleInstallment installment = null;
|
LoanRepaymentScheduleInstallment installment = null;
|
||||||
while (iterator.hasNext() && (installment == null || installment.isAdditional() || installment.isDownPayment())) {
|
while (iterator.hasNext() && (installment == null || installment.isAdditional() || installment.isDownPayment())) {
|
||||||
@@ -1723,7 +1729,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
} else {
|
} else {
|
||||||
if (loanProductRelatedDetail != null && loanProductRelatedDetail.isAllowFullTermForTranche()
|
if (loanProductRelatedDetail != null && loanProductRelatedDetail.isAllowFullTermForTranche()
|
||||||
&& loanProductRelatedDetail.getNumberOfRepayments() > 0 && !rm.getDueDate().isBefore(transactionDate)) {
|
&& loanProductRelatedDetail.getNumberOfRepayments() > 0 && !rm.getDueDate().isBefore(transactionDate)) {
|
||||||
installmentCounter.getAndIncrement();
|
if (installment == null || !installment.isAdditional()) {
|
||||||
|
installmentCounter.getAndIncrement();
|
||||||
|
}
|
||||||
final LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(
|
final LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(
|
||||||
loanTransaction.getLoan(), installmentCounter.get(), rm.getFromDate(), rm.getDueDate(),
|
loanTransaction.getLoan(), installmentCounter.get(), rm.getFromDate(), rm.getDueDate(),
|
||||||
rm.getDuePrincipal().getAmount(), rm.getDueInterest().getAmount(), null, null, null, null, null, null,
|
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);
|
newInstallment.updateObligationsMet(currency, transactionDate);
|
||||||
iterator.add(newInstallment);
|
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,
|
private void moveRelatedChargesToInstallment(Set<LoanCharge> charges, LoanRepaymentScheduleInstallment target,
|
||||||
List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency) {
|
List<LoanRepaymentScheduleInstallment> sources, MonetaryCurrency currency) {
|
||||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(sources);
|
||||||
Set<LoanCharge> chargesOfNewInstallment = getLoanChargesOfInstallment(charges, target, firstNormalInstallmentNumber);
|
Set<LoanCharge> chargesOfNewInstallment = getLoanChargesOfInstallment(charges, target, firstNormalInstallmentNumber);
|
||||||
Integer targetInstallmentNumber = target.getInstallmentNumber();
|
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 -> {
|
.filter(source -> source != target).ifPresent(source -> {
|
||||||
// move fees
|
// move fees
|
||||||
chargesOfNewInstallment.stream().filter(LoanCharge::isNotFullyPaid).filter(LoanCharge::isFeeCharge)
|
chargesOfNewInstallment.stream().filter(LoanCharge::isNotFullyPaid).filter(LoanCharge::isFeeCharge)
|
||||||
|
|||||||
+29
@@ -19,10 +19,13 @@
|
|||||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
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.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||||
@@ -50,6 +53,7 @@ public class ProgressiveTransactionCtx extends TransactionCtx {
|
|||||||
@Setter
|
@Setter
|
||||||
private boolean isPrepayAttempt = false;
|
private boolean isPrepayAttempt = false;
|
||||||
private final List<LoanRepaymentScheduleInstallment> skipRepaymentScheduleInstallments = new ArrayList<>();
|
private final List<LoanRepaymentScheduleInstallment> skipRepaymentScheduleInstallments = new ArrayList<>();
|
||||||
|
private final List<Long> processedLoanChargeIds;
|
||||||
|
|
||||||
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
||||||
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
|
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
|
||||||
@@ -62,8 +66,33 @@ public class ProgressiveTransactionCtx extends TransactionCtx {
|
|||||||
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
|
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
|
||||||
ProgressiveLoanInterestScheduleModel model, Money sumOfInterestRefundAmount,
|
ProgressiveLoanInterestScheduleModel model, Money sumOfInterestRefundAmount,
|
||||||
List<LoanTermVariations> activeLoanTermVariations) {
|
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);
|
super(currency, installments, charges, overpaymentHolder, changedTransactionDetail, activeLoanTermVariations);
|
||||||
this.sumOfInterestRefundAmount = sumOfInterestRefundAmount;
|
this.sumOfInterestRefundAmount = sumOfInterestRefundAmount;
|
||||||
this.model = model;
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user