FINERACT-2181: Fix for certain interest recalculation settings making prepay api and payment allocation inconsistent
This commit is contained in:
+61
-17
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
|
||||
import static java.math.BigDecimal.ZERO;
|
||||
import static java.util.stream.Collectors.mapping;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrualAdjustment;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrueTransaction;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK;
|
||||
@@ -67,6 +68,7 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
@@ -74,6 +76,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
|
||||
@@ -96,6 +99,7 @@ import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
|
||||
@@ -103,7 +107,6 @@ import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreCloseInterestCalculationStrategy;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||
import org.apache.fineract.util.LoopContext;
|
||||
@@ -326,6 +329,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
public ChangedTransactionDetail processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
// If we are behind, we might need to first recalculate interest
|
||||
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx) {
|
||||
if (loanTransaction.isRepaymentLikeType() && loanTransaction.isNotReversed()) {
|
||||
progressiveTransactionCtx.setPrepayAttempt(calculateIsPrepayAttempt(loanTransaction, progressiveTransactionCtx));
|
||||
}
|
||||
recalculateInterestForDate(loanTransaction.getTransactionDate(), progressiveTransactionCtx);
|
||||
}
|
||||
switch (loanTransaction.getTypeOf()) {
|
||||
@@ -1523,13 +1529,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
|
||||
LocalDate lastOverdueBalanceChange;
|
||||
if (currentInstallment.getLoan().getLoanInterestRecalculationDetails().getRestFrequencyType().isSameAsRepayment()) {
|
||||
if (shouldRecalculateTillInstallmentDueDate(currentInstallment.getLoan().getLoanInterestRecalculationDetails(),
|
||||
ctx.isPrepayAttempt())) {
|
||||
lastOverdueBalanceChange = toDate;
|
||||
} else {
|
||||
lastOverdueBalanceChange = currentDate;
|
||||
}
|
||||
emiCalculator.addBalanceCorrection(ctx.getModel(), lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
|
||||
ctx.getModel().lastOverdueBalanceChange(lastOverdueBalanceChange);
|
||||
if (DateUtils.isBefore(ctx.getModel().lastOverdueBalanceChange(), toDate)) {
|
||||
emiCalculator.addBalanceCorrection(ctx.getModel(), lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
|
||||
ctx.getModel().lastOverdueBalanceChange(lastOverdueBalanceChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2419,7 +2428,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
if (DueType.IN_ADVANCE.equals(paymentAllocationType.getDueType())) {
|
||||
payDate = calculateNewPayDateInCaseOfInAdvancePayment(loanTransaction, installment);
|
||||
payDate = calculateNewPayDateInCaseOfInAdvancePayment(loanTransaction, installment, ctx.isPrepayAttempt());
|
||||
updateRepaymentPeriodBalances(paymentAllocationType, installment, ctx, payDate);
|
||||
}
|
||||
|
||||
@@ -2465,19 +2474,54 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate calculateNewPayDateInCaseOfInAdvancePayment(LoanTransaction loanTransaction,
|
||||
LoanRepaymentScheduleInstallment inAdvanceInstallment) {
|
||||
LoanPreCloseInterestCalculationStrategy strategy = loanTransaction.getLoan().getLoanInterestRecalculationDetails()
|
||||
.getPreCloseInterestCalculationStrategy();
|
||||
private boolean calculateIsPrepayAttempt(LoanTransaction loanTransaction, ProgressiveTransactionCtx progressiveTransactionCtx) {
|
||||
Loan loan = loanTransaction.getLoan();
|
||||
LoanRepaymentScheduleInstallment installment = loan.getRelatedRepaymentScheduleInstallment(loanTransaction.getTransactionDate());
|
||||
if (installment == null) {
|
||||
return false;
|
||||
}
|
||||
if (loan.getLoanInterestRecalculationDetails() == null) {
|
||||
return false;
|
||||
}
|
||||
OutstandingDetails outstandingAmounts = emiCalculator.getOutstandingAmountsTillDate(progressiveTransactionCtx.getModel(),
|
||||
installment.getDueDate());
|
||||
OutstandingAmountsDTO result = new OutstandingAmountsDTO(progressiveTransactionCtx.getCurrency()) //
|
||||
.principal(outstandingAmounts.getOutstandingPrincipal()) //
|
||||
.interest(outstandingAmounts.getOutstandingInterest());//
|
||||
|
||||
return switch (strategy) {
|
||||
case TILL_PRE_CLOSURE_DATE -> loanTransaction.getTransactionDate();
|
||||
// TODO use isInPeriod
|
||||
case TILL_REST_FREQUENCY_DATE -> loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getFromDate()) //
|
||||
&& !loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getDueDate()) //
|
||||
? inAdvanceInstallment.getDueDate() //
|
||||
: loanTransaction.getTransactionDate(); //
|
||||
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
|
||||
return loanTransaction.getAmount(progressiveTransactionCtx.getCurrency()).isGreaterThanOrEqualTo(result.getTotalOutstanding());
|
||||
}
|
||||
|
||||
private LocalDate calculateNewPayDateInCaseOfInAdvancePayment(LoanTransaction loanTransaction,
|
||||
LoanRepaymentScheduleInstallment inAdvanceInstallment, boolean prepayAttempt) {
|
||||
if (shouldRecalculateTillInstallmentDueDate(loanTransaction.getLoan().getLoanInterestRecalculationDetails(), prepayAttempt)) {
|
||||
return isInPeriod(loanTransaction.getTransactionDate(), inAdvanceInstallment, false) ? inAdvanceInstallment.getDueDate()
|
||||
: loanTransaction.getTransactionDate();
|
||||
} else {
|
||||
return loanTransaction.getTransactionDate();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldRecalculateTillInstallmentDueDate(LoanInterestRecalculationDetails recalculationDetails,
|
||||
boolean isPrepayAttempt) {
|
||||
// Rest frequency type and pre close interest calculation strategy can be controversial
|
||||
// if restFrequencyType == DAILY and preCloseInterestCalculationStrategy == TILL_PRE_CLOSURE_DATE
|
||||
// no problem. Calculate till transaction date
|
||||
// if restFrequencyType == SAME_AS_REPAYMENT_PERIOD and preCloseInterestCalculationStrategy ==
|
||||
// TILL_REST_FREQUENCY_DATE
|
||||
// again, no problem. Calculate till due date of current installment
|
||||
// if restFrequencyType == DAILY and preCloseInterestCalculationStrategy == TILL_REST_FREQUENCY_DATE
|
||||
// or restFrequencyType == SAME_AS_REPAYMENT_PERIOD and preCloseInterestCalculationStrategy ==
|
||||
// TILL_PRE_CLOSURE_DATE
|
||||
// we cannot harmonize the two configs. Behaviour should mimic prepay api.
|
||||
return switch (recalculationDetails.getRestFrequencyType()) {
|
||||
case DAILY ->
|
||||
isPrepayAttempt && recalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled();
|
||||
case SAME_AS_REPAYMENT_PERIOD ->
|
||||
recalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled();
|
||||
case WEEKLY -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WEEKLY");
|
||||
case MONTHLY -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: MONTHLY");
|
||||
case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -44,6 +44,8 @@ public class ProgressiveTransactionCtx extends TransactionCtx {
|
||||
private boolean isChargedOff = false;
|
||||
@Setter
|
||||
private boolean isContractTerminated = false;
|
||||
@Setter
|
||||
private boolean isPrepayAttempt = false;
|
||||
private List<LoanRepaymentScheduleInstallment> skipRepaymentScheduleInstallments = new ArrayList<>();
|
||||
|
||||
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
Optional<ProgressiveLoanInterestScheduleModel> savedModel = interestScheduleModelRepositoryWrapper.getSavedModel(loan,
|
||||
transactionDate);
|
||||
ProgressiveLoanInterestScheduleModel model = savedModel
|
||||
.orElseGet(() -> processor.calculateInterestScheduleModel(loan.getId(), onDate));
|
||||
.orElseGet(() -> processor.calculateInterestScheduleModel(loan.getId(), transactionDate));
|
||||
OutstandingDetails outstandingAmounts = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate);
|
||||
// TODO: We should add all the past due outstanding amounts as well
|
||||
OutstandingAmountsDTO result = new OutstandingAmountsDTO(currency) //
|
||||
|
||||
Reference in New Issue
Block a user