This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit ef80634b4e8513f0f3147cc40da781ec14dc4d34 Author: Adam Saghy <adamsa...@gmail.com> AuthorDate: Thu Sep 25 21:05:18 2025 +0200 FINERACT-2381: Fix Accrual Activity calculation --- ...tLoanRepaymentScheduleTransactionProcessor.java | 51 -------------- .../LoanAccrualActivityProcessingService.java | 3 + ...dvancedPaymentScheduleTransactionProcessor.java | 5 -- .../LoanAccrualActivityProcessingServiceImpl.java | 78 ++++++++++++++++++++++ .../ReprocessLoanTransactionsServiceImpl.java | 2 + 5 files changed, 83 insertions(+), 56 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 7e8adf046d..aa20e1fe40 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -223,63 +223,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen reprocessChargebackTransactionRelation(changedTransactionDetail, transactionsToBeProcessed); } else if (loanTransaction.isChargeOff()) { recalculateChargeOffTransaction(changedTransactionDetail, loanTransaction, currency, installments); - } else if (loanTransaction.isAccrualActivity()) { - recalculateAccrualActivityTransaction(changedTransactionDetail, loanTransaction, currency, installments); } } reprocessInstallments(disbursementDate, transactionsToBeProcessed, installments, currency); return changedTransactionDetail; } - protected void calculateAccrualActivity(LoanTransaction loanTransaction, MonetaryCurrency currency, - List<LoanRepaymentScheduleInstallment> installments) { - - final int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); - - final Optional<LoanRepaymentScheduleInstallment> currentInstallmentOpt = installments.stream() - .filter(installment -> LoanRepaymentScheduleProcessingWrapper.isInPeriod(loanTransaction.getTransactionDate(), installment, - installment.getInstallmentNumber().equals(firstNormalInstallmentNumber))) - .findFirst(); - - if (currentInstallmentOpt.isEmpty()) { - return; - } - - final LoanRepaymentScheduleInstallment currentInstallment = currentInstallmentOpt.get(); - if (currentInstallment.isNotFullyPaidOff() && (currentInstallment.getDueDate().isAfter(loanTransaction.getTransactionDate()) - || (currentInstallment.getDueDate().isEqual(loanTransaction.getTransactionDate()) - && loanTransaction.getTransactionDate().equals(DateUtils.getBusinessLocalDate())))) { - loanTransaction.reverse(); - } else { - loanTransaction.resetDerivedComponents(); - final Money principalPortion = Money.zero(currency); - Money interestPortion = currentInstallment.getInterestCharged(currency); - Money feeChargesPortion = currentInstallment.getFeeChargesCharged(currency); - Money penaltyChargesPortion = currentInstallment.getPenaltyChargesCharged(currency); - if (interestPortion.plus(feeChargesPortion).plus(penaltyChargesPortion).isZero()) { - loanTransaction.reverse(); - } else { - loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); - final Loan loan = loanTransaction.getLoan(); - if ((loan.isClosedObligationsMet() || loanBalanceService.isOverPaid(loan)) && currentInstallment.isObligationsMet() - && currentInstallment.isTransactionDateWithinPeriod(currentInstallment.getObligationsMetOnDate())) { - loanTransaction.updateTransactionDate(currentInstallment.getObligationsMetOnDate()); - } - } - } - } - - private void recalculateAccrualActivityTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction, - MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments) { - final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); - - calculateAccrualActivity(newLoanTransaction, currency, installments); - - if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { - createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail); - } - } - @Override public ChangedTransactionDetail processLatestTransaction(final LoanTransaction loanTransaction, final TransactionCtx ctx) { switch (loanTransaction.getTypeOf()) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingService.java index a3c233915b..aa4769f5f5 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingService.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.time.LocalDate; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.springframework.lang.NonNull; import org.springframework.transaction.annotation.Transactional; @@ -30,6 +31,8 @@ public interface LoanAccrualActivityProcessingService { void makeAccrualActivityTransaction(@NonNull Loan loan, @NonNull LocalDate currentDate); + void recalculateAccrualActivityTransaction(Loan loan, ChangedTransactionDetail changedTransactionDetail); + @Transactional void processAccrualActivityForLoanClosure(@NonNull Loan loan); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 85970a72f7..a9a9bd1cae 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -412,7 +412,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed."); case REAMORTIZE -> handleReAmortization(loanTransaction, ctx); case REAGE -> handleReAge(loanTransaction, ctx); - case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx); case CAPITALIZED_INCOME -> handleCapitalizedIncome(loanTransaction, ctx); case CONTRACT_TERMINATION -> handleContractTermination(loanTransaction, ctx); // TODO: Cover rest of the transaction types @@ -2903,10 +2902,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep reprocessInstallments(installments); } - protected void calculateAccrualActivity(LoanTransaction transaction, TransactionCtx ctx) { - super.calculateAccrualActivity(transaction, ctx.getCurrency(), ctx.getInstallments()); - } - private void reprocessInstallments(final List<LoanRepaymentScheduleInstallment> installments) { final AtomicInteger counter = new AtomicInteger(1); final AtomicReference<LocalDate> previousDueDate = new AtomicReference<>(null); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java index 754857d921..2d531c9a7c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,9 +37,14 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustT import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountService; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; @@ -101,6 +107,32 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi }); } + @Override + public void recalculateAccrualActivityTransaction(Loan loan, ChangedTransactionDetail changedTransactionDetail) { + List<LoanTransaction> accrualActivities = loanTransactionRepository.findNonReversedByLoanAndType(loan, + LoanTransactionType.ACCRUAL_ACTIVITY); + accrualActivities.forEach(accrualActivity -> { + final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(accrualActivity); + + calculateAccrualActivity(newLoanTransaction, loan.getCurrency(), loan.getRepaymentScheduleInstallments()); + + if (!LoanTransaction.transactionAmountsMatch(loan.getCurrency(), accrualActivity, newLoanTransaction)) { + createNewTransaction(accrualActivity, newLoanTransaction, changedTransactionDetail); + } + }); + } + + protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction, + ChangedTransactionDetail changedTransactionDetail) { + loanTransaction.reverse(); + loanTransaction.updateExternalId(null); + newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations()); + // Adding Replayed relation from newly created transaction to reversed transaction + newLoanTransaction.getLoanTransactionRelations().add( + LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED)); + changedTransactionDetail.addTransactionChange(new TransactionChangeData(loanTransaction, newLoanTransaction)); + } + @Override @Transactional public void processAccrualActivityForLoanClosure(final @NonNull Loan loan) { @@ -206,6 +238,52 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi } } + private void calculateAccrualActivity(LoanTransaction loanTransaction, MonetaryCurrency currency, + List<LoanRepaymentScheduleInstallment> installments) { + + final int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); + + final List<LoanRepaymentScheduleInstallment> targetInstallments = installments.stream() + .filter(installment -> LoanRepaymentScheduleProcessingWrapper.isInPeriod(loanTransaction.getTransactionDate(), installment, + installment.getInstallmentNumber().equals(firstNormalInstallmentNumber)) + || (DateUtils.isEqual(installment.getObligationsMetOnDate(), loanTransaction.getTransactionDate()) + && installment.getDueDate().isAfter(loanTransaction.getTransactionDate()))) + .toList(); + + if (targetInstallments.isEmpty()) { + return; + } + + AtomicBoolean isReset = new AtomicBoolean(false); + targetInstallments.forEach(currentInstallment -> { + if (currentInstallment.isNotFullyPaidOff() && (currentInstallment.getDueDate().isAfter(loanTransaction.getTransactionDate()) + || (currentInstallment.getDueDate().isEqual(loanTransaction.getTransactionDate()) + && loanTransaction.getTransactionDate().equals(DateUtils.getBusinessLocalDate())))) { + loanTransaction.reverse(); + } else { + if (!isReset.get()) { + loanTransaction.resetDerivedComponents(); + isReset.set(true); + } + final Money principalPortion = Money.zero(currency); + Money interestPortion = currentInstallment.getInterestCharged(currency); + Money feeChargesPortion = currentInstallment.getFeeChargesCharged(currency); + Money penaltyChargesPortion = currentInstallment.getPenaltyChargesCharged(currency); + + loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + final Loan loan = loanTransaction.getLoan(); + if ((loan.isClosedObligationsMet() || loanBalanceService.isOverPaid(loan)) && currentInstallment.isObligationsMet() + && currentInstallment.isTransactionDateWithinPeriod(currentInstallment.getObligationsMetOnDate())) { + loanTransaction.updateTransactionDate(currentInstallment.getObligationsMetOnDate()); + } + } + }); + if (MathUtil.isZero(MathUtil.nullToZero(MathUtil.add(loanTransaction.getInterestPortion(), loanTransaction.getFeeChargesPortion(), + loanTransaction.getPenaltyChargesPortion())))) { + loanTransaction.reverse(); + } + } + private Map<LocalDate, List<LoanTransaction>> loadExistingAccrualActivitiesByDate(final @NonNull Loan loan, final List<LoanRepaymentScheduleInstallment> installments) { final Set<LocalDate> dueDates = installments.stream().map(LoanRepaymentScheduleInstallment::getDueDate).collect(Collectors.toSet()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java index de8fe0a830..b35e1c0253 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java @@ -62,6 +62,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa private final LoanTransactionService loanTransactionService; private final LoanJournalEntryPoster loanJournalEntryPoster; private final BusinessEventNotifierService businessEventNotifierService; + private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService; @Override public void reprocessTransactions(final Loan loan) { @@ -227,6 +228,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa .map(TransactionChangeData::getNewTransaction).toList(); loan.getLoanTransactions().addAll(newTransactions); loanBalanceService.updateLoanSummaryDerivedFields(loan); + loanAccrualActivityProcessingService.recalculateAccrualActivityTransaction(loan, changedTransactionDetail); return changedTransactionDetail; } }