FINERACT-2232: add income capitalization principal handling and repayment schedule calculations on loans - E2E tests

This commit is contained in:
Peter Kovacs
2025-05-27 18:35:39 +02:00
committed by Adam Saghy
parent aac62a92bb
commit 8d7d8ca7ef
7 changed files with 1318 additions and 99 deletions
@@ -81,8 +81,6 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
loan.addLoanTransaction(capitalizedIncomeTransaction);
// Recalculate loan transactions
recalculateLoanTransactions(loan, transactionDate, capitalizedIncomeTransaction);
// Create capitalized income balances
createCapitalizedIncomeBalance(capitalizedIncomeTransaction);
// Save and flush (PK is set)
loanTransactionRepository.saveAndFlush(capitalizedIncomeTransaction);
// Create capitalized income balances
@@ -132,6 +130,6 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
capitalizedIncomeBalance.setDate(capitalizedIncomeTransaction.getTransactionDate());
capitalizedIncomeBalance.setAmount(capitalizedIncomeTransaction.getAmount());
capitalizedIncomeBalance.setUnrecognizedAmount(capitalizedIncomeTransaction.getAmount());
capitalizedIncomeBalanceRepository.save(capitalizedIncomeBalance);
capitalizedIncomeBalanceRepository.saveAndFlush(capitalizedIncomeBalance);
}
}
@@ -18,14 +18,13 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;
import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional;
import java.time.LocalDate;
import java.util.Optional;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
@@ -47,8 +46,7 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
private final ProgressiveLoanModelRepository loanModelRepository;
private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService;
private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor;
@PersistenceContext
private final EntityManager entityManager;
private final FlushModeHandler flushModeHandler;
@Transactional
@Override
@@ -57,9 +55,7 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
return null;
}
String jsonModel = progressiveLoanInterestScheduleModelParserService.toJson(model);
try {
// Note: FlushMode set because otherwise it could cause a concurrent writing exception
entityManager.setFlushMode(FlushModeType.COMMIT);
flushModeHandler.withFlushMode(FlushModeType.COMMIT, () -> {
ProgressiveLoanModel progressiveLoanModel = loanModelRepository.findOneByLoanId(loan.getId()).orElseGet(() -> {
ProgressiveLoanModel plm = new ProgressiveLoanModel();
plm.setLoan(loan);
@@ -69,15 +65,17 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
progressiveLoanModel.setLastModifiedDate(DateUtils.getAuditOffsetDateTime());
progressiveLoanModel.setJsonModel(jsonModel);
loanModelRepository.save(progressiveLoanModel);
} finally {
entityManager.setFlushMode(FlushModeType.AUTO);
}
});
return jsonModel;
}
@Override
public Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId) {
return loanModelRepository.findOneByLoanId(loanId);
final Optional[] progressiveLoanModel = new Optional[1];
flushModeHandler.withFlushMode(FlushModeType.COMMIT, () -> {
progressiveLoanModel[0] = loanModelRepository.findOneByLoanId(loanId);
});
return progressiveLoanModel[0];
}
@Override
@@ -90,40 +88,27 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
@Override
public boolean hasValidModelForDate(Long loanId, LocalDate targetDate) {
try {
// Note: FlushMode set because an unwanted flush caused reverse replay new transactions
entityManager.setFlushMode(FlushModeType.COMMIT);
Optional<ProgressiveLoanModel> progressiveLoanModel = findOneByLoanId(loanId);
LocalDate modelUpdatedTilDate = progressiveLoanModel.map(ProgressiveLoanModel::getBusinessDate).orElse(null);
return progressiveLoanModel.isPresent() && !targetDate.isBefore(modelUpdatedTilDate);
} finally {
entityManager.setFlushMode(FlushModeType.AUTO);
}
Optional<ProgressiveLoanModel> progressiveLoanModel = findOneByLoanId(loanId);
LocalDate modelUpdatedTilDate = progressiveLoanModel.map(ProgressiveLoanModel::getBusinessDate).orElse(null);
return progressiveLoanModel.isPresent() && !targetDate.isBefore(modelUpdatedTilDate);
}
@Override
public Optional<ProgressiveLoanInterestScheduleModel> getSavedModel(Loan loan, LocalDate businessDate) {
try {
// Note: FlushMode set because an unwanted flush caused reverse replay new transactions
entityManager.setFlushMode(FlushModeType.COMMIT);
Optional<ProgressiveLoanModel> progressiveLoanModel = findOneByLoanId(loan.getId());
Optional<ProgressiveLoanInterestScheduleModel> savedModel;
if (progressiveLoanModel.isPresent() && !progressiveLoanModel.get().getBusinessDate().isAfter(businessDate)) {
savedModel = extractModel(progressiveLoanModel);
if (savedModel.isPresent() && progressiveLoanModel.get().getBusinessDate().isBefore(businessDate)) {
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), Set.of(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
new ChangedTransactionDetail(), savedModel.get());
ctx.setChargedOff(loan.isChargedOff());
advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(businessDate, ctx);
}
} else {
savedModel = Optional.empty();
Optional<ProgressiveLoanModel> progressiveLoanModel = findOneByLoanId(loan.getId());
Optional<ProgressiveLoanInterestScheduleModel> savedModel;
if (progressiveLoanModel.isPresent() && !progressiveLoanModel.get().getBusinessDate().isAfter(businessDate)) {
savedModel = extractModel(progressiveLoanModel);
if (savedModel.isPresent() && progressiveLoanModel.get().getBusinessDate().isBefore(businessDate)) {
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
Set.of(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), savedModel.get());
ctx.setChargedOff(loan.isChargedOff());
advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(businessDate, ctx);
}
return savedModel;
} finally {
entityManager.setFlushMode(FlushModeType.AUTO);
} else {
savedModel = Optional.empty();
}
return savedModel;
}
@Override
@@ -255,8 +255,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
private void addCredit(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount,
Money creditedInterestAmount) {
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount, scheduleModel.zero())
.ifPresent(repaymentPeriod -> {
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount,
scheduleModel.zero()).ifPresent(repaymentPeriod -> {
addCreditedAmountsToInterestPeriod(scheduleModel, transactionDate, creditedPrincipalAmount, creditedInterestAmount);
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
calculateOutstandingBalance(scheduleModel);
@@ -76,14 +76,15 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
}
public static InterestPeriod empty(@NotNull RepaymentPeriod repaymentPeriod, MathContext mc) {
return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null,null, mc, false);
return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null, null, mc, false);
}
public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) {
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), interestPeriod.getMc(), interestPeriod.isPaused());
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), interestPeriod.getMc(),
interestPeriod.isPaused());
}
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,