FINERACT-2398: Add handling for equal overdue interest split re-amortization

This commit is contained in:
Oleksii Novikov
2025-12-12 20:02:06 +01:00
committed by Arnold Gálovics
parent a229a55c49
commit 36d779eabf
6 changed files with 252 additions and 104 deletions
@@ -605,10 +605,74 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
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 handleReamortizationWithEqualAmortizationInterestSplitHandlingType(final LoanTransaction loanTransaction,
final TransactionCtx transactionCtx) {
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = progressiveTransactionCtx.getCurrency();
final List<LoanRepaymentScheduleInstallment> previousInstallments = progressiveTransactionCtx.getInstallments().stream()
.filter(installment -> !installment.getDueDate().isAfter(transactionDate)).toList();
final List<LoanRepaymentScheduleInstallment> futureInstallments = progressiveTransactionCtx.getInstallments().stream()
.filter(installment -> installment.getDueDate().isAfter(transactionDate))
.filter(installment -> !installment.isAdditional() && !installment.isDownPayment() && !installment.isReAged()).toList();
Money totalOverDuePrincipal = Money.zero(currency);
Money totalOverDueFee = Money.zero(currency);
for (LoanRepaymentScheduleInstallment installment : previousInstallments) {
final Money outstandingPrincipal = emiCalculator
.findRepaymentPeriod(progressiveTransactionCtx.getModel(), installment.getFromDate(), installment.getDueDate())
.map(RepaymentPeriod::getOutstandingPrincipal).map(amount -> Money.of(currency, amount.getAmount()))
.orElse(Money.zero(currency));
totalOverDuePrincipal = totalOverDuePrincipal.add(outstandingPrincipal);
totalOverDueFee = totalOverDueFee.add(installment.getFeeChargesOutstanding(currency));
installment.setFeeChargesCharged(installment.getFeeChargesPaid(currency).getAmount());
}
final Money totalOverDueInterest = emiCalculator.getOutstandingInterestTillDate(progressiveTransactionCtx.getModel(),
previousInstallments.getLast().getDueDate());
loanTransaction.resetDerivedComponents();
loanTransaction.updateComponentsAndTotal(totalOverDuePrincipal, totalOverDueInterest, Money.zero(currency),
Money.zero(currency));
emiCalculator.updateModelRepaymentPeriodsDuringReAmortizationWithEqualInterestSplit(progressiveTransactionCtx.getModel(),
transactionDate);
updateInstallmentsByRepaymentPeriods(loanTransaction, progressiveTransactionCtx);
distributeFeeAmongFutureInstallments(totalOverDueFee, futureInstallments);
} else {
// TODO: implement interestRecalculation = false logic
throw new UnsupportedOperationException(
"Logic for re-amortization when interest bearing loan has interestRecalculation disabled is not implemented");
}
}
private void distributeFeeAmongFutureInstallments(final Money totalOverDueFee,
final List<LoanRepaymentScheduleInstallment> futureInstallments) {
if (totalOverDueFee.isZero() || futureInstallments.isEmpty()) {
return;
}
final int numberOfFutureInstallments = futureInstallments.size();
final MonetaryCurrency currency = totalOverDueFee.getCurrency();
BigDecimal feePortionPerInstallment = totalOverDueFee.getAmount().divide(BigDecimal.valueOf(numberOfFutureInstallments),
MoneyHelper.getMathContext());
final LoanRepaymentScheduleInstallment lastFutureInstallment = futureInstallments.stream()
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).get();
BigDecimal remainingFee = totalOverDueFee.getAmount();
for (final LoanRepaymentScheduleInstallment installment : futureInstallments) {
if (lastFutureInstallment.equals(installment)) {
installment.addToFeeCharges(Money.of(currency, remainingFee));
} else {
installment.addToFeeCharges(Money.of(currency, feePortionPerInstallment));
remainingFee = remainingFee.subtract(feePortionPerInstallment);
}
}
}
private void handleReamortizationWithWaiveInterestHandlingType(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
@@ -172,4 +172,7 @@ public interface EMICalculator {
LocalDate targetRepaymentPeriodDueDate, LocalDate newDueDate);
void updateModelRepaymentPeriodsDuringReAmortization(ProgressiveLoanInterestScheduleModel model, LocalDate transactionDate);
void updateModelRepaymentPeriodsDuringReAmortizationWithEqualInterestSplit(ProgressiveLoanInterestScheduleModel model,
LocalDate transactionDate);
}
@@ -685,6 +685,83 @@ public final class ProgressiveEMICalculator implements EMICalculator {
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(model, reAmortizedPeriods);
}
@Override
public void updateModelRepaymentPeriodsDuringReAmortizationWithEqualInterestSplit(final ProgressiveLoanInterestScheduleModel model,
final LocalDate transactionDate) {
final MathContext mc = model.mc();
final List<RepaymentPeriod> periodsBeforeTransactionDate = model.repaymentPeriods().stream()
.filter(rp -> !rp.getDueDate().isAfter(transactionDate) && !rp.isFullyPaid()).toList();
Money totalAmortizedInterest = model.zero();
for (RepaymentPeriod rp : periodsBeforeTransactionDate) {
totalAmortizedInterest = totalAmortizedInterest.add(rp.getDueInterest().minus(rp.getPaidInterest(), mc), mc);
}
final List<RepaymentPeriod> futurePeriods = model.repaymentPeriods().stream().filter(rp -> rp.getDueDate().isAfter(transactionDate))
.toList();
final int numberOfFuturePeriods = futurePeriods.size();
Money interestPortionPerPeriod = model.zero();
if (numberOfFuturePeriods > 0 && totalAmortizedInterest.isGreaterThanZero()) {
interestPortionPerPeriod = totalAmortizedInterest.dividedBy(numberOfFuturePeriods, mc);
}
model.repaymentPeriods().forEach(rp -> rp.getInterestPeriods().forEach(ip -> {
if (!ip.getBalanceCorrectionAmount().isZero()) {
ip.addBalanceCorrectionAmount(ip.getBalanceCorrectionAmount().negated());
}
}));
final List<RepaymentPeriod> allPeriods = model.repaymentPeriods();
for (int i = 0; i < allPeriods.size(); i++) {
final RepaymentPeriod rp = allPeriods.get(i);
if (rp.isFullyPaid() && !rp.getDueDate().isAfter(transactionDate)) {
Money nextPeriodCreditedPrincipal = model.zero();
if (i + 1 < allPeriods.size()) {
nextPeriodCreditedPrincipal = allPeriods.get(i + 1).getCreditedPrincipal();
}
final Money effectivePaidPrincipal = rp.getPaidPrincipal().minus(nextPeriodCreditedPrincipal, mc);
if (effectivePaidPrincipal.isGreaterThanZero()) {
final InterestPeriod lastInterestPeriod = rp.getInterestPeriods().getLast();
lastInterestPeriod.addBalanceCorrectionAmount(effectivePaidPrincipal.negated());
}
}
}
moveOutstandingAmountsFromPeriodsBeforeTransactionDateForEqualInterestSplit(periodsBeforeTransactionDate, transactionDate);
calculateRateFactorForPeriods(futurePeriods, model);
for (final RepaymentPeriod futurePeriod : futurePeriods) {
futurePeriod.setReAgedInterest(MathUtil.plus(futurePeriod.getReAgedInterest(), interestPortionPerPeriod, mc));
}
calculateEMIOnActualModel(futurePeriods, model);
calculateOutstandingBalance(model);
calculateLastUnpaidRepaymentPeriodEMI(model, transactionDate);
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(model, futurePeriods);
}
private void moveOutstandingAmountsFromPeriodsBeforeTransactionDateForEqualInterestSplit(
final List<RepaymentPeriod> periodsBeforeTransactionDate, final LocalDate transactionDate) {
periodsBeforeTransactionDate.forEach(rp -> {
rp.getInterestPeriods().stream().filter(ip -> ip.getDueDate().isEqual(transactionDate)).forEach(ip -> {
if (!ip.getBalanceCorrectionAmount().isZero()) {
ip.addBalanceCorrectionAmount(ip.getBalanceCorrectionAmount().negated());
}
});
final InterestPeriod lastInterestPeriod = rp.getInterestPeriods().getLast();
Money paidPrincipal = rp.getPaidPrincipal();
if (paidPrincipal.isGreaterThanZero()) {
lastInterestPeriod.addBalanceCorrectionAmount(paidPrincipal.negated());
}
rp.setEmi(rp.getTotalPaidAmount());
rp.moveOutstandingDueToReAging();
rp.setNoUnrecognisedInterest(true);
});
}
private LocalDate calculateDateForFixedLength(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LoanApplicationTerms loanApplicationTerms, final LocalDate targetRepaymentPeriodDueDate, final LocalDate newDueDate) {
final long variationDays = DateUtils.getDifferenceInDays(targetRepaymentPeriodDueDate, newDueDate);