FINERACT-2398: Re-amortization:- Interest handling-Default behavior

This commit is contained in:
mariiaKraievska
2025-12-08 11:54:16 +01:00
committed by Adam Saghy
parent 8391614330
commit 3c4d8da1c8
8 changed files with 532 additions and 46 deletions
@@ -96,6 +96,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepayme
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.domain.reamortization.LoanReAmortizationParameter;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
@@ -590,6 +591,42 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
private void handleReAmortization(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
final Loan loan = loanTransaction.getLoan();
final LoanReAmortizationParameter loanReAmortizationParameter = loanTransaction.getLoanReAmortizationParameter();
if (loan.isInterestBearing()) {
switch (loanReAmortizationParameter.getInterestHandlingType()) {
case DEFAULT -> handleReamortizationWithDefaultInterestHandlingType(loanTransaction, transactionCtx);
case WAIVE_INTEREST -> handleReamortizationWithWaiveInterestHandlingType(loanTransaction, transactionCtx);
case EQUAL_AMORTIZATION_INTEREST_SPLIT ->
handleReamortizationWithEqualAmortizationInterestSplitHandlingType(loanTransaction, transactionCtx);
}
} else {
reamortizationOnNonInterestBearingLoan(loanTransaction, transactionCtx);
}
}
private void handleReamortizationWithEqualAmortizationInterestSplitHandlingType(LoanTransaction loanTransaction,
TransactionCtx transactionCtx) {
throw new UnsupportedOperationException(
"EQUAL_AMORTIZATION_INTEREST_SPLIT interest handling strategy for re-amortization is not implemented");
}
private void handleReamortizationWithWaiveInterestHandlingType(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
throw new UnsupportedOperationException("WAIVE_INTEREST interest handling strategy for re-amortization is not implemented");
}
private void handleReamortizationWithDefaultInterestHandlingType(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
// re-amortization logic for interest-bearing loans
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
handleReAmortizationWithInterestRecalculationEnabled(loanTransaction, progressiveTransactionCtx);
}
// TODO: implement interestRecalculation = false logic
throw new UnsupportedOperationException(
"Logic for re-amortization when interest bearing loan has interestRecalculation disabled is not implemented");
}
private static void reamortizationOnNonInterestBearingLoan(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
LocalDate transactionDate = loanTransaction.getTransactionDate();
List<LoanRepaymentScheduleInstallment> previousInstallments = transactionCtx.getInstallments().stream() //
.filter(installment -> !installment.getDueDate().isAfter(transactionDate)) //
@@ -634,6 +671,32 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
private void handleReAmortizationWithInterestRecalculationEnabled(final LoanTransaction loanTransaction,
final ProgressiveTransactionCtx ctx) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = ctx.getCurrency();
final List<LoanRepaymentScheduleInstallment> previousInstallments = ctx.getInstallments().stream() //
.filter(installment -> !installment.getDueDate().isAfter(transactionDate)) //
.toList();
Money totalOverDuePrincipal = Money.zero(currency);
for (LoanRepaymentScheduleInstallment installment : previousInstallments) {
final Money principalCompleted = installment.getPrincipalCompleted(currency);
totalOverDuePrincipal = totalOverDuePrincipal.add(installment.getPrincipal(currency).minus(principalCompleted));
}
final Money totalOverDueInterest = emiCalculator.getOutstandingInterestTillDate(ctx.getModel(),
previousInstallments.getLast().getDueDate());
loanTransaction.resetDerivedComponents();
loanTransaction.updateComponentsAndTotal(totalOverDuePrincipal, totalOverDueInterest, Money.zero(currency), Money.zero(currency));
// Update the existing model with re-amortized periods
emiCalculator.updateModelRepaymentPeriodsDuringReAmortization(ctx.getModel(), transactionDate);
updateInstallmentsByRepaymentPeriods(loanTransaction, ctx);
}
@Override
protected void handleChargeback(LoanTransaction loanTransaction, TransactionCtx ctx) {
processCreditTransaction(loanTransaction, ctx);
@@ -671,12 +734,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
emiCalculator.creditPrincipal(model, transactionDate, transactionAmount);
updateRepaymentPeriods(loanTransaction, progressiveTransactionCtx);
updateInstallmentsByRepaymentPeriods(loanTransaction, progressiveTransactionCtx);
if (repaidAmount.isGreaterThanZero()) {
emiCalculator.payPrincipal(model, currentInstallment.getFromDate(), currentInstallment.getDueDate(),
transactionDate, repaidAmount);
updateRepaymentPeriods(loanTransaction, progressiveTransactionCtx);
updateInstallmentsByRepaymentPeriods(loanTransaction, progressiveTransactionCtx);
currentInstallment.payPrincipalComponent(transactionDate, repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney));
@@ -709,12 +772,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed.getLast();
emiCalculator.creditPrincipal(model, transactionDate, transactionAmount);
updateRepaymentPeriods(loanTransaction, progressiveTransactionCtx);
updateInstallmentsByRepaymentPeriods(loanTransaction, progressiveTransactionCtx);
if (repaidAmount.isGreaterThanZero()) {
emiCalculator.payPrincipal(model, currentInstallment.getFromDate(), currentInstallment.getDueDate(),
transactionDate, repaidAmount);
updateRepaymentPeriods(loanTransaction, progressiveTransactionCtx);
updateInstallmentsByRepaymentPeriods(loanTransaction, progressiveTransactionCtx);
currentInstallment.payPrincipalComponent(transactionDate, repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney));
@@ -866,7 +929,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
// update repayment periods until maturity date, for principal and interest portions
updateRepaymentPeriods(loanTransaction, ctx);
updateInstallmentsByRepaymentPeriods(loanTransaction, ctx);
// search last instalment if transaction is posted on last installment's due date
// otherwise search installment where transaction date is in from date (inclusive) to due date (exclusive)
@@ -2589,15 +2652,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (PRINCIPAL.equals(paymentAllocationType.getAllocationType())) {
emiCalculator.payPrincipal(model, installment.getFromDate(), installment.getDueDate(), payDate, paidPortion);
updateRepaymentPeriods(loanTransaction, ctx);
updateInstallmentsByRepaymentPeriods(loanTransaction, ctx);
} else if (INTEREST.equals(paymentAllocationType.getAllocationType())) {
emiCalculator.payInterest(model, installment.getFromDate(), installment.getDueDate(), payDate, paidPortion);
updateRepaymentPeriods(loanTransaction, ctx);
updateInstallmentsByRepaymentPeriods(loanTransaction, ctx);
}
return paidPortion;
}
private void updateRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx) {
private void updateInstallmentsByRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx) {
ctx.getModel().repaymentPeriods().forEach(rm -> {
LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream().filter(
ri -> ri.getFromDate().equals(rm.getFromDate()) && ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment())
@@ -170,4 +170,6 @@ public interface EMICalculator {
void changeDueDate(ProgressiveLoanInterestScheduleModel scheduleModel, LoanApplicationTerms loanApplicationTerms,
LocalDate targetRepaymentPeriodDueDate, LocalDate newDueDate);
void updateModelRepaymentPeriodsDuringReAmortization(ProgressiveLoanInterestScheduleModel model, LocalDate transactionDate);
}
@@ -570,7 +570,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LocalDate reAgePeriodStartDate, final LocalDate reAgeFirstDueDate, final LocalDate targetDate,
final LoanApplicationTerms loanApplicationTerms, final MathContext mc) {
moveOutstandingAmountsFromPeriodsBeforeReAging(scheduleModel.repaymentPeriods(), targetDate);
moveOutstandingAmountsFromPeriodsBeforeTransactionDate(scheduleModel.repaymentPeriods(), targetDate);
// calculate already paid balances from transaction date
final OutstandingDetails paidBalancesFromTransactionDate = calculatePaidBalancesAfterDate(scheduleModel, targetDate);
@@ -673,6 +673,18 @@ public final class ProgressiveEMICalculator implements EMICalculator {
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, targetRepaymentPeriodDueDate);
}
@Override
public void updateModelRepaymentPeriodsDuringReAmortization(final ProgressiveLoanInterestScheduleModel model,
final LocalDate transactionDate) {
moveOutstandingAmountsFromPeriodsBeforeTransactionDate(model.repaymentPeriods(), transactionDate);
final List<RepaymentPeriod> reAmortizedPeriods = model.repaymentPeriods().stream()
.filter(rp -> rp.getDueDate().isAfter(transactionDate)).toList();
calculateEMIOnActualModel(reAmortizedPeriods, model);
calculateOutstandingBalance(model);
calculateLastUnpaidRepaymentPeriodEMI(model, transactionDate);
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(model, reAmortizedPeriods);
}
private LocalDate calculateDateForFixedLength(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LoanApplicationTerms loanApplicationTerms, final LocalDate targetRepaymentPeriodDueDate, final LocalDate newDueDate) {
final long variationDays = DateUtils.getDifferenceInDays(targetRepaymentPeriodDueDate, newDueDate);
@@ -825,16 +837,16 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
/**
* * Zeroing out the EMI of the repayment periods, that are before re-aging and not been fully paid. And decreases
* the balance correction amount (added during interest recalculation for the business date) by the amount of the
* principal that was moved.
* * Zeroing out the EMI of the repayment periods, that are before the transaction date and not been fully paid. And
* decreases the balance correction amount (added during interest recalculation for the business date) by the amount
* of the principal that was moved.
*/
private static void moveOutstandingAmountsFromPeriodsBeforeReAging(final List<RepaymentPeriod> existingRepaymentPeriods,
private static void moveOutstandingAmountsFromPeriodsBeforeTransactionDate(final List<RepaymentPeriod> existingRepaymentPeriods,
final LocalDate transactionDate) {
final List<RepaymentPeriod> periodsBeforeReAging = existingRepaymentPeriods.stream()
final List<RepaymentPeriod> periodsBeforeTransactionDate = existingRepaymentPeriods.stream()
.filter(rp -> !rp.getFromDate().isAfter(transactionDate) && !rp.isFullyPaid()).toList();
periodsBeforeReAging.forEach(rp -> {
periodsBeforeTransactionDate.forEach(rp -> {
rp.getInterestPeriods().stream().filter(ip -> ip.getDueDate().isEqual(transactionDate)).forEach(ip -> {
if (!ip.getBalanceCorrectionAmount().isZero()) {
ip.addBalanceCorrectionAmount(ip.getBalanceCorrectionAmount().negated());
@@ -845,7 +857,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
lastInterestPeriod.addBalanceCorrectionAmount(rp.getOutstandingPrincipal().negated());
}
rp.setEmi(rp.getTotalPaidAmount());
rp.setOutstandingMovedDueToReAging(true);
rp.setOutstandingMoved(true);
});
}
@@ -1725,7 +1737,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
rp.getInterestPeriods().getLast()
.addCreditedInterestAmount(MathUtil.min(rp.getOutstandingInterest(), rp.getCreditedInterest(), false).negated());
rp.setEmi(rp.getTotalPaidAmount());
rp.setOutstandingMovedDueToReAging(true);
rp.setOutstandingMoved(true);
});
// stop calculate unrecognised interest at this point because all
@@ -91,7 +91,7 @@ public class RepaymentPeriod {
@Getter
@Setter
private boolean isOutstandingMovedDueToReAging = false;
private boolean isOutstandingMoved = false;
@Setter
@Getter
@@ -148,7 +148,7 @@ public class RepaymentPeriod {
repaymentPeriod.getPaidPrincipal(), repaymentPeriod.getPaidInterest(), repaymentPeriod.getFutureUnrecognizedInterest(), mc,
repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(), repaymentPeriod.isReAged(),
repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
newRepaymentPeriod.setOutstandingMovedDueToReAging(repaymentPeriod.isOutstandingMovedDueToReAging());
newRepaymentPeriod.setOutstandingMoved(repaymentPeriod.isOutstandingMoved());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
newRepaymentPeriod.getInterestPeriods().add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc));
@@ -162,7 +162,7 @@ public class RepaymentPeriod {
repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), zero, zero,
zero, mc, repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(),
repaymentPeriod.isReAged(), repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
newRepaymentPeriod.setOutstandingMovedDueToReAging(repaymentPeriod.isOutstandingMovedDueToReAging());
newRepaymentPeriod.setOutstandingMoved(repaymentPeriod.isOutstandingMoved());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod);
@@ -311,8 +311,7 @@ public class RepaymentPeriod {
* @return
*/
public Money getTotalCreditedAmount() {
return isOutstandingMovedDueToReAging ? Money.zero(getCurrency(), getMc())
: getCreditedPrincipal().plus(getCreditedInterest(), getMc());
return isOutstandingMoved ? Money.zero(getCurrency(), getMc()) : getCreditedPrincipal().plus(getCreditedInterest(), getMc());
}
/**