FINERACT-2220: Use stored ProgressiveLoanInterestScheduleModel for processLatestTransaction if applicable

This commit is contained in:
Soma Sörös
2025-05-16 10:03:54 +02:00
committed by Adam Saghy
parent 1fc2aae654
commit cd03dc020e
9 changed files with 129 additions and 51 deletions
@@ -92,7 +92,6 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanRefundRequestData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
@@ -109,6 +108,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
@@ -160,6 +160,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final LoanAccountService loanAccountService;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private final LoanTransactionProcessingService loanTransactionProcessingService;
@Transactional
@Override
@@ -889,28 +890,21 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = transactionProcessorFactory
.determineProcessor(loan.getTransactionProcessingStrategyCode());
final LoanRepaymentScheduleInstallment currentInstallment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(transactionDate);
boolean reprocess = loan.isInterestBearingAndInterestRecalculationEnabled() //
|| loan.isForeclosure() //
|| !isTransactionChronologicallyLatest //
|| !DateUtils.isEqualBusinessDate(transactionDate) //
|| currentInstallment == null //
|| !currentInstallment.getTotalOutstanding(loan.getCurrency()).isEqualTo(refundTransaction.getAmount(loan.getCurrency())); //
if (!reprocess) {
loan.getLoanTransactions().add(refundTransaction);
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(refundTransaction,
boolean processLatest = isTransactionChronologicallyLatest //
&& !loan.isForeclosure() //
&& loanTransactionProcessingService.canProcessLatestTransactionOnly(loan, refundTransaction, currentInstallment); //
if (processLatest) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), refundTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.getLoanTransactions().add(refundTransaction);
if (interestRefundTransaction != null) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(),
interestRefundTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.addLoanTransaction(interestRefundTransaction);
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(interestRefundTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
}
} else {
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
@@ -924,12 +918,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
reprocessLoanTransactionsService.reprocessTransactions(loan);
}
// Store and flush newly created transaction to generate PK
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(refundTransaction);
if (interestRefundTransaction != null) {
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(interestRefundTransaction);
}
// Store and flush newly created transaction to generate PK
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(refundTransaction);
if (interestRefundTransaction != null) {
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(interestRefundTransaction);
}
loanLifecycleStateMachine.determineAndTransition(loan, transactionDate);
@@ -38,9 +38,12 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
@@ -59,11 +62,57 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
private final LoanTermVariationsMapper loanMapper;
private final InterestScheduleModelRepositoryWrapper modelRepository;
@Override
public boolean canProcessLatestTransactionOnly(Loan loan, LoanTransaction loanTransaction,
LoanRepaymentScheduleInstallment currentInstallment) {
if (!loan.isInterestBearingAndInterestRecalculationEnabled()) {
return true;
}
if (!DateUtils.isEqualBusinessDate(loanTransaction.getTransactionDate())) {
return false;
}
if (loan.isProgressiveSchedule()) {
return modelRepository.hasValidModelForDate(loan.getId(), loanTransaction.getTransactionDate());
}
return currentInstallment != null
&& currentInstallment.getTotalOutstanding(loan.getCurrency()).isEqualTo(loanTransaction.getAmount(loan.getCurrency()));
}
private ChangedTransactionDetail processLatestTransactionProgressiveInterestRecalculation(
AdvancedPaymentScheduleTransactionProcessor advancedProcessor, Loan loan, LoanTransaction loanTransaction) {
Optional<ProgressiveLoanInterestScheduleModel> savedModel = modelRepository.getSavedModel(loan,
loanTransaction.getTransactionDate());
ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
new ChangedTransactionDetail(), savedModel.orElse(null), getTotalRefundInterestAmount(loan));
progressiveContext.getAlreadyProcessedTransactions().addAll(loan.retrieveListOfTransactionsForReprocessing());
progressiveContext.setChargedOff(loan.isChargedOff());
ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext);
if (savedModel.isPresent() && !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
modelRepository.writeInterestScheduleModel(loan, savedModel.get());
}
return result;
}
private Money getTotalRefundInterestAmount(Loan loan) {
List<LoanTransactionType> supportedInterestRefundTransactionTypes = loan.getSupportedInterestRefundTransactionTypes();
if (supportedInterestRefundTransactionTypes != null && supportedInterestRefundTransactionTypes.isEmpty()) {
return Money.zero(loan.getCurrency());
}
return loan.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed).filter(LoanTransaction::isInterestRefund)
.map(t -> t.getAmount(loan.getCurrency())).reduce(Money.zero(loan.getCurrency()), Money::add);
}
@Override
public ChangedTransactionDetail processLatestTransaction(String transactionProcessingStrategyCode, LoanTransaction loanTransaction,
TransactionCtx ctx) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(
transactionProcessingStrategyCode);
if (loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedProcessor
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
return processLatestTransactionProgressiveInterestRecalculation(advancedProcessor, loanTransaction.getLoan(), loanTransaction);
}
return loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, ctx);
}