FINERACT-2326: Fix payment allocation when current period is closed

This commit is contained in:
mark.vituska
2025-07-28 14:23:54 +02:00
committed by Adam Saghy
parent 70373eafef
commit 68b35024f6
5 changed files with 104 additions and 60 deletions
@@ -1570,10 +1570,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} else {
lastOverdueBalanceChange = currentDate;
}
if (DateUtils.isBefore(ctx.getModel().lastOverdueBalanceChange(), toDate)) {
emiCalculator.addBalanceCorrection(ctx.getModel(), lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
ctx.getModel().lastOverdueBalanceChange(lastOverdueBalanceChange);
}
emiCalculator.addBalanceCorrection(ctx.getModel(), lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
ctx.getModel().lastOverdueBalanceChange(lastOverdueBalanceChange);
}
}
@@ -1599,10 +1597,32 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
loanTransaction.resetDerivedComponents();
}
calculateUnrecognizedInterestForClosedPeriodByInterestRecalculationStrategy(loanTransaction, transactionCtx);
Money transactionAmountUnprocessed = loanTransaction.getAmount(transactionCtx.getCurrency());
processTransaction(loanTransaction, transactionCtx, transactionAmountUnprocessed);
}
private void calculateUnrecognizedInterestForClosedPeriodByInterestRecalculationStrategy(LoanTransaction loanTransaction,
TransactionCtx transactionCtx) {
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx && progressiveTransactionCtx.isPrepayAttempt()
&& loanTransaction.isRepaymentLikeType() && loanTransaction.getLoan().getLoanInterestRecalculationDetails()
.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled()) {
Optional<RepaymentPeriod> oCurrentRepaymentPeriod = progressiveTransactionCtx.getModel().repaymentPeriods().stream()
.filter(rm -> DateUtils.isDateInRangeFromInclusiveToExclusive(rm.getFromDate(), rm.getDueDate(),
loanTransaction.getTransactionDate()))
.findFirst();
if (oCurrentRepaymentPeriod.isPresent() && oCurrentRepaymentPeriod.get().isFullyPaid()) {
RepaymentPeriod currentRepaymentPeriod = oCurrentRepaymentPeriod.get();
OutstandingDetails outstandingAmountsTillDate = emiCalculator
.getOutstandingAmountsTillDate(progressiveTransactionCtx.getModel(), currentRepaymentPeriod.getDueDate());
if (outstandingAmountsTillDate.getOutstandingInterest().isGreaterThanZero()) {
currentRepaymentPeriod.setFutureUnrecognizedInterest(outstandingAmountsTillDate.getOutstandingInterest());
}
}
}
}
private LoanTransactionToRepaymentScheduleMapping getTransactionMapping(
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, LoanTransaction loanTransaction,
LoanRepaymentScheduleInstallment currentInstallment, MonetaryCurrency currency) {
@@ -1751,26 +1771,28 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
final LoanRepaymentScheduleInstallment currentInstallment = loan.getRelatedRepaymentScheduleInstallment(transactionDate);
if (!installments.isEmpty() && transactionDate.isBefore(loan.getMaturityDate())) {
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
final BigDecimal interestOutstanding = currentInstallment.getInterestOutstanding(loan.getCurrency()).getAmount();
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
currentInstallment.getDueDate(), transactionDate, true).getAmount();
if (interestOutstanding.compareTo(BigDecimal.ZERO) > 0 || newInterest.compareTo(BigDecimal.ZERO) > 0) {
currentInstallment.updateInterestCharged(newInterest);
}
} else {
final BigDecimal totalInterest = currentInstallment.getInterestOutstanding(transactionCtx.getCurrency()).getAmount();
if (totalInterest.compareTo(BigDecimal.ZERO) > 0) {
final long totalDaysInPeriod = ChronoUnit.DAYS.between(currentInstallment.getFromDate(),
currentInstallment.getDueDate());
final long daysTillChargeOff = ChronoUnit.DAYS.between(currentInstallment.getFromDate(), transactionDate);
if (currentInstallment.isNotFullyPaidOff()) {
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
final BigDecimal interestOutstanding = currentInstallment.getInterestOutstanding(loan.getCurrency()).getAmount();
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
currentInstallment.getDueDate(), transactionDate, true).getAmount();
if (interestOutstanding.compareTo(BigDecimal.ZERO) > 0 || newInterest.compareTo(BigDecimal.ZERO) > 0) {
currentInstallment.updateInterestCharged(newInterest);
}
} else {
final BigDecimal totalInterest = currentInstallment.getInterestOutstanding(transactionCtx.getCurrency()).getAmount();
if (totalInterest.compareTo(BigDecimal.ZERO) > 0) {
final long totalDaysInPeriod = ChronoUnit.DAYS.between(currentInstallment.getFromDate(),
currentInstallment.getDueDate());
final long daysTillChargeOff = ChronoUnit.DAYS.between(currentInstallment.getFromDate(), transactionDate);
final MathContext mc = MoneyHelper.getMathContext();
final Money interestTillChargeOff = Money.of(transactionCtx.getCurrency(), totalInterest
.divide(BigDecimal.valueOf(totalDaysInPeriod), mc).multiply(BigDecimal.valueOf(daysTillChargeOff), mc), mc);
final MathContext mc = MoneyHelper.getMathContext();
final Money interestTillChargeOff = Money.of(transactionCtx.getCurrency(), totalInterest
.divide(BigDecimal.valueOf(totalDaysInPeriod), mc).multiply(BigDecimal.valueOf(daysTillChargeOff), mc), mc);
currentInstallment.updateInterestCharged(interestTillChargeOff.getAmount());
currentInstallment.updateInterestCharged(interestTillChargeOff.getAmount());
}
}
}
@@ -1790,6 +1812,21 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
currentInstallment.updatePrincipal(MathUtil.nullToZero(currentInstallment.getPrincipal()).add(futurePrincipal));
if (currentInstallment.isObligationsMet()) {
final BigDecimal futureOutstandingPrincipal = futureInstallments.stream()
.map(installment -> installment.getPrincipalOutstanding(transactionCtx.getCurrency()).getAmount())
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
final BigDecimal futureTotalPaidInAdvance = futureInstallments.stream()
.map(LoanRepaymentScheduleInstallment::getTotalPaidInAdvance).filter(Objects::nonNull)
.reduce(ZERO, BigDecimal::add);
currentInstallment
.setPrincipalCompleted(MathUtil.nullToZero(currentInstallment.getPrincipal()).subtract(futureOutstandingPrincipal));
currentInstallment.setTotalPaidInAdvance(
MathUtil.nullToZero(currentInstallment.getTotalPaidInAdvance()).add(futureTotalPaidInAdvance));
}
final List<LoanRepaymentScheduleInstallment> installmentsUpToTransactionDate = installments.stream()
.filter(installment -> transactionDate.isAfter(installment.getFromDate()))
.collect(Collectors.toCollection(ArrayList::new));
@@ -1817,7 +1854,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
transactionCtx.getInstallments().stream().filter(installment -> installment.getFromDate().isBefore(transactionDate))
transactionCtx.getInstallments().stream()
.filter(installment -> installment.getFromDate().isBefore(transactionDate) && installment.isNotFullyPaidOff())
.filter(installment -> transactionsToBeReprocessed.stream()
.anyMatch(transaction -> transaction.getLoanTransactionToRepaymentScheduleMappings().stream().anyMatch(
mapping -> mapping.getInstallment().getInstallmentNumber().equals(installment.getInstallmentNumber()))))
@@ -1835,8 +1873,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
newTransaction.updateLoan(loan);
loan.getLoanTransactions().add(newTransaction);
}
loanBalanceService.updateLoanSummaryDerivedFields(loan);
}
loanBalanceService.updateLoanSummaryDerivedFields(loan);
}
}
@@ -2870,9 +2908,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.filter(repaymentPeriod -> previousInstallmentsMapping.stream()
.anyMatch(installment -> installment.getFromDate().equals(repaymentPeriod.getFromDate())))
.forEach(RepaymentPeriod::resetDerivedComponents);
emiCalculator.addBalanceCorrection(transactionCtx.getModel(), processTransaction.getTransactionDate(),
processTransaction.getPrincipalPortion(transactionCtx.getCurrency()));
processSingleTransaction(processTransaction, transactionCtx);
LoanRepaymentScheduleInstallment installment = processTransaction.getLoan()
.getRelatedRepaymentScheduleInstallment(processTransaction.getTransactionDate());
if (installment == null || installment.isNotFullyPaidOff()) {
emiCalculator.addBalanceCorrection(transactionCtx.getModel(), processTransaction.getTransactionDate(),
processTransaction.getPrincipalPortion(transactionCtx.getCurrency()));
processSingleTransaction(processTransaction, transactionCtx);
}
}
}