diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/domain/AbstractAuditableWithUTCDateTimeCustom.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/domain/AbstractAuditableWithUTCDateTimeCustom.java index 9798cbf8e..6612602bf 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/domain/AbstractAuditableWithUTCDateTimeCustom.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/domain/AbstractAuditableWithUTCDateTimeCustom.java @@ -52,11 +52,11 @@ public abstract class AbstractAuditableWithUTCDateTimeCustom newTransactionMappings = new LinkedHashMap<>(); + private final List transactionChanges = new ArrayList<>(); - private final Map currentTransactionToOldId = new LinkedHashMap<>(); + public void addTransactionChange(final TransactionChangeData transactionChangeData) { + for (TransactionChangeData change : transactionChanges) { + if (transactionChangeData.getOldTransaction() != null && change.getOldTransaction() != null + && Objects.equals(change.getOldTransaction().getId(), transactionChangeData.getOldTransaction().getId())) { + change.setOldTransaction(transactionChangeData.getOldTransaction()); + change.setNewTransaction(transactionChangeData.getNewTransaction()); + return; + } else if (transactionChangeData.getOldTransaction() == null && change.getOldTransaction() == null + && change.getNewTransaction() != null + && Objects.equals(change.getNewTransaction().getId(), transactionChangeData.getNewTransaction().getId())) { + change.setNewTransaction(transactionChangeData.getNewTransaction()); + return; + } + } + transactionChanges.add(transactionChangeData); + } + public void addNewTransactionChangeBeforeExistingOne(final TransactionChangeData newTransactionChange, + final LoanTransaction existingLoanTransaction) { + if (existingLoanTransaction != null) { + final Optional existingChange = transactionChanges.stream().filter( + change -> change.getNewTransaction() != null && Objects.equals(change.getNewTransaction(), existingLoanTransaction)) + .findFirst(); + + if (existingChange.isPresent()) { + transactionChanges.add(transactionChanges.indexOf(existingChange.get()), newTransactionChange); + return; + } + } + transactionChanges.add(newTransactionChange); + } + + public void removeTransactionChange(final LoanTransaction newTransaction) { + transactionChanges.removeIf(change -> change.getNewTransaction().equals(newTransaction)); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index feccefffc..240bb33bb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -107,6 +107,7 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; +import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData; 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; @@ -719,10 +720,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(this); + for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) { + change.getNewTransaction().updateLoan(this); } - this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values()); + final List newTransactions = changedTransactionDetail.getTransactionChanges().stream() + .map(TransactionChangeData::getNewTransaction).toList(); + this.loanTransactions.addAll(newTransactions); updateLoanSummaryDerivedFields(); return changedTransactionDetail; } @@ -2656,14 +2659,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(this); + for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) { + change.getNewTransaction().updateLoan(this); } /* * Commented since throwing exception if external id present for one of the transactions. for this need to save * the reversed transactions first and then new transactions. */ - this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values()); + final List newTransactions = changedTransactionDetail.getTransactionChanges().stream() + .map(TransactionChangeData::getNewTransaction).toList(); + this.loanTransactions.addAll(newTransactions); updateLoanSummaryDerivedFields(); return changedTransactionDetail; 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 722328cd2..765df9f39 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 @@ -25,7 +25,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -34,6 +33,7 @@ import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail; +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.LoanCharge; @@ -187,7 +187,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen **/ if (newLoanTransaction.isReversed()) { loanTransaction.reverse(); - changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), loanTransaction); + changedTransactionDetail.addTransactionChange(new TransactionChangeData(loanTransaction, loanTransaction)); } else if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { loanTransaction.updateLoanTransactionToRepaymentScheduleMappings( newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings()); @@ -264,7 +264,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } @Override - public void processLatestTransaction(final LoanTransaction loanTransaction, final TransactionCtx ctx) { + public ChangedTransactionDetail processLatestTransaction(final LoanTransaction loanTransaction, final TransactionCtx ctx) { switch (loanTransaction.getTypeOf()) { case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments()); case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges()); @@ -288,6 +288,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } } } + return ctx.getChangedTransactionDetail(); } @Override @@ -421,10 +422,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen private void reprocessChargebackTransactionRelation(ChangedTransactionDetail changedTransactionDetail, List transactionsToBeProcessed) { - List mergedTransactionList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail); - for (Map.Entry entry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - if (entry.getValue().isChargeback()) { + for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) { + LoanTransaction newTransaction = change.getNewTransaction(); + LoanTransaction oldTransaction = change.getOldTransaction(); + + if (newTransaction.isChargeback()) { for (LoanTransaction loanTransaction : mergedTransactionList) { if (loanTransaction.isReversed()) { continue; @@ -433,8 +436,9 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen LoanTransactionRelation oldLoanTransactionRelation = null; for (LoanTransactionRelation transactionRelation : loanTransaction.getLoanTransactionRelations()) { if (LoanTransactionRelationTypeEnum.CHARGEBACK.equals(transactionRelation.getRelationType()) - && entry.getKey().equals(transactionRelation.getToTransaction().getId())) { - newLoanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, entry.getValue(), + && oldTransaction != null && oldTransaction.getId() != null + && oldTransaction.getId().equals(transactionRelation.getToTransaction().getId())) { + newLoanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, newTransaction, LoanTransactionRelationTypeEnum.CHARGEBACK); oldLoanTransactionRelation = transactionRelation; break; @@ -511,8 +515,9 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen private List getMergedTransactionList(List transactionList, ChangedTransactionDetail changedTransactionDetail) { - List mergedList = new ArrayList<>(changedTransactionDetail.getNewTransactionMappings().values()); - mergedList.addAll(new ArrayList<>(transactionList)); + List mergedList = new ArrayList<>( + changedTransactionDetail.getTransactionChanges().stream().map(TransactionChangeData::getNewTransaction).toList()); + mergedList.addAll(transactionList); return mergedList; } @@ -525,7 +530,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen // Adding Replayed relation from newly created transaction to reversed transaction newLoanTransaction.getLoanTransactionRelations().add( LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED)); - changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction); + changedTransactionDetail.addTransactionChange(new TransactionChangeData(loanTransaction, newLoanTransaction)); } protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHolder overpaymentHolder, MonetaryCurrency currency, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java index f49635f62..411f26309 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java @@ -39,8 +39,10 @@ public interface LoanRepaymentScheduleTransactionProcessor { /** * Provides support for processing the latest transaction (which should be the latest transaction) against the loan * schedule. + * + * @return ChangedTransactionDetail */ - void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx); + ChangedTransactionDetail processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx); /** * Provides support for passing all {@link LoanTransaction}'s so it will completely re-process the entire loan diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java index 6ac3ca3d1..f31cbe692 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java @@ -18,14 +18,15 @@ */ package org.apache.fineract.portfolio.loanaccount.service; -import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; -import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; @RequiredArgsConstructor public class ReplayedTransactionBusinessEventServiceImpl implements ReplayedTransactionBusinessEventService { @@ -35,18 +36,30 @@ public class ReplayedTransactionBusinessEventServiceImpl implements ReplayedTran @Override public void raiseTransactionReplayedEvents(final ChangedTransactionDetail changedTransactionDetail) { - if (changedTransactionDetail == null || changedTransactionDetail.getNewTransactionMappings().isEmpty()) { + if (changedTransactionDetail == null || changedTransactionDetail.getTransactionChanges().isEmpty()) { return; } // Extra safety net to avoid event leaking try { businessEventNotifierService.startExternalEventRecording(); - for (Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - LoanTransaction oldTransaction = loanTransactionRepository.findById(mapEntry.getKey()) - .orElseThrow(() -> new LoanTransactionNotFoundException(mapEntry.getKey())); - LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(oldTransaction); - data.setNewTransactionDetail(mapEntry.getValue()); - businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data)); + + for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) { + final LoanTransaction newTransaction = change.getNewTransaction(); + final LoanTransaction oldTransaction = change.getOldTransaction(); + + if (oldTransaction != null) { + final LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(oldTransaction); + data.setNewTransactionDetail(newTransaction); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data)); + } else { + if (newTransaction.isAccrual()) { + businessEventNotifierService + .notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(newTransaction)); + } else { + businessEventNotifierService + .notifyPostBusinessEvent(new LoanAccrualAdjustmentTransactionBusinessEvent(newTransaction)); + } + } } businessEventNotifierService.stopExternalEventRecording(); } catch (Exception e) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsService.java index bc84ad804..ed11c5ee7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsService.java @@ -19,17 +19,23 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.time.LocalDate; +import java.util.List; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; public interface ReprocessLoanTransactionsService { void reprocessTransactions(Loan loan); + void reprocessTransactions(Loan loan, List loanTransactions); + void reprocessTransactionsWithPostTransactionChecks(Loan loan, LocalDate transactionDate); void processPostDisbursementTransactions(Loan loan); void removeLoanCharge(Loan loan, LoanCharge loanCharge); + void processLatestTransaction(LoanTransaction loanTransaction, Loan loan); + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java index d29867e52..334fedbc1 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java @@ -19,14 +19,18 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.time.LocalDate; -import java.util.Map; +import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.portfolio.interestpauses.service.LoanAccountTransfersService; +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.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +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.springframework.stereotype.Service; @Service @@ -45,6 +49,22 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa } } + @Override + public void reprocessTransactions(final Loan loan, final List loanTransactions) { + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor(); + final ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( + loan.getDisbursementDate(), loanTransactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), + loan.getActiveCharges()); + for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) { + change.getNewTransaction().updateLoan(loan); + } + final List newTransactions = changedTransactionDetail.getTransactionChanges().stream() + .map(TransactionChangeData::getNewTransaction).toList(); + loan.getLoanTransactions().addAll(newTransactions); + loan.updateLoanSummaryDerivedFields(); + handleChangedDetail(changedTransactionDetail); + } + @Override public void reprocessTransactionsWithPostTransactionChecks(final Loan loan, final LocalDate transactionDate) { final ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactionsWithPostTransactionChecks(transactionDate); @@ -63,12 +83,30 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa loan.removeLoanCharge(loanCharge).ifPresent(this::handleChangedDetail); } + @Override + public void processLatestTransaction(final LoanTransaction loanTransaction, final Loan loan) { + final ChangedTransactionDetail changedTransactionDetail = loan.getTransactionProcessor().processLatestTransaction(loanTransaction, + new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), + new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail())); + final List newTransactions = changedTransactionDetail.getTransactionChanges().stream() + .map(TransactionChangeData::getNewTransaction).peek(transaction -> transaction.updateLoan(loan)).toList(); + loan.getLoanTransactions().addAll(newTransactions); + + loan.updateLoanSummaryDerivedFields(); + handleChangedDetail(changedTransactionDetail); + } + private void handleChangedDetail(final ChangedTransactionDetail changedTransactionDetail) { - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); - loanAccountTransfersService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) { + final LoanTransaction newTransaction = change.getNewTransaction(); + final LoanTransaction oldTransaction = change.getOldTransaction(); + + loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(newTransaction); + + if (oldTransaction != null) { + loanAccountTransfersService.updateLoanTransaction(oldTransaction.getId(), newTransaction); + } } replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } - } 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 14dde4885..6d25be7b3 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 @@ -38,7 +38,6 @@ import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -65,13 +64,11 @@ import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.MathUtil; -import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent; -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.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +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.LoanCharge; @@ -88,7 +85,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; @@ -120,9 +116,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private final EMICalculator emiCalculator; private final LoanRepositoryWrapper loanRepositoryWrapper; private final InterestRefundService interestRefundService; - private final LoanTransactionRepository loanTransactionRepository; private final ExternalIdFactory externalIdFactory; - private final BusinessEventNotifierService businessEventNotifierService; @Override public String getCode() { @@ -227,11 +221,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } } - Map newTransactionMappings = changedTransactionDetail.getNewTransactionMappings(); - for (Long oldTransactionId : newTransactionMappings.keySet()) { - LoanTransaction oldTransaction = loanTransactions.stream().filter(e -> oldTransactionId.equals(e.getId())).findFirst().get(); - LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId); - createNewTransaction(oldTransaction, newTransaction, ctx); + final List transactionChanges = changedTransactionDetail.getTransactionChanges(); + + for (TransactionChangeData change : transactionChanges) { + LoanTransaction oldTransaction = change.getOldTransaction(); + LoanTransaction newTransaction = change.getNewTransaction(); + + if (oldTransaction != null) { + createNewTransaction(oldTransaction, newTransaction, ctx); + } } recalculateInterestForDate(targetDate, ctx); List txs = changeOperations.stream() // @@ -263,9 +261,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } @NotNull - private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) { - LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId()); - return newTransaction == null ? transaction : newTransaction; + private static LoanTransaction getProcessedTransaction(final ChangedTransactionDetail changedTransactionDetail, + final LoanTransaction transaction) { + return changedTransactionDetail.getTransactionChanges().stream() + .filter(change -> change.getOldTransaction() != null && change.getOldTransaction().getId() != null + && change.getOldTransaction().getId().equals(transaction.getId())) + .map(TransactionChangeData::getNewTransaction).findFirst().orElse(transaction); } private void processInterestRateChange(final List installments, @@ -298,7 +299,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } @Override - public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) { + public ChangedTransactionDetail processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) { // If we are behind, we might need to first recalculate interest if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx) { recalculateInterestForDate(loanTransaction.getTransactionDate(), progressiveTransactionCtx); @@ -320,10 +321,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep case REAGE -> handleReAge(loanTransaction, ctx); case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx); // TODO: Cover rest of the transaction types - default -> { - log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf()); - } + default -> log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf()); } + return ctx.getChangedTransactionDetail(); } private void handleInterestRefund(LoanTransaction loanTransaction, TransactionCtx ctx) { @@ -408,14 +408,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail(); Long chargebackId = chargebackTransaction.getId(); // this the normal case without reverse-replay if (changedTransactionDetail != null) { + final List transactionChanges = changedTransactionDetail.getTransactionChanges(); if (chargebackId == null) { // the chargeback transaction was changed, so we need to look it up from the ctx. - chargebackId = changedTransactionDetail.getCurrentTransactionToOldId().get(chargebackTransaction); + chargebackId = transactionChanges.stream().filter(change -> change.getNewTransaction().equals(chargebackTransaction)) + .flatMap(change -> Optional.ofNullable(change.getOldTransaction()).map(AbstractPersistableCustom::getId).stream()) + .findFirst().orElse(null); } Long toId = chargebackId; - Collection updatedTransactions = changedTransactionDetail.getNewTransactionMappings().values(); - Optional fromTransaction = updatedTransactions.stream() + Optional fromTransaction = changedTransactionDetail.getTransactionChanges().stream() + .map(TransactionChangeData::getNewTransaction) .filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(hasMatchingToLoanTransaction(toId, CHARGEBACK)) || tr.getLoanTransactionRelations().stream() .anyMatch(this.hasMatchingToLoanTransaction(chargebackTransaction, CHARGEBACK))) @@ -656,16 +659,22 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep // Remove the current chargeback from the list allTransactions.remove(chargebackTransaction); - if (ctx.getChangedTransactionDetail() != null) { - Long oldId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(chargebackTransaction); - allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), oldId)).findFirst().ifPresent(allTransactions::remove); - } + final ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail(); + if (changedTransactionDetail != null) { + final List transactionChanges = changedTransactionDetail.getTransactionChanges(); - // Add the replayed transactions and remove their old version before the replay - if (ctx.getChangedTransactionDetail() != null && ctx.getChangedTransactionDetail().getNewTransactionMappings() != null) { - for (Long id : ctx.getChangedTransactionDetail().getNewTransactionMappings().keySet()) { - allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), id)).findFirst().ifPresent(allTransactions::remove); - allTransactions.add(ctx.getChangedTransactionDetail().getNewTransactionMappings().get(id)); + transactionChanges.stream().filter(change -> change.getNewTransaction().equals(chargebackTransaction)) + .map(TransactionChangeData::getOldTransaction).filter(Objects::nonNull).findFirst().ifPresent(allTransactions::remove); + + // Add the replayed transactions and remove their old version before the replay + for (TransactionChangeData change : transactionChanges) { + LoanTransaction oldTransaction = change.getOldTransaction(); + LoanTransaction newTransaction = change.getNewTransaction(); + + if (oldTransaction != null) { + allTransactions.removeIf(tr -> Objects.equals(tr.getId(), oldTransaction.getId())); + } + allTransactions.add(newTransaction); } } @@ -823,7 +832,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep // For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has // changed. processTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); - ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().put(processTransaction, loanTransaction.getId()); + ctx.getChangedTransactionDetail().addTransactionChange(new TransactionChangeData(loanTransaction, processTransaction)); } // Reset derived component of new loan transaction and re-process transaction processLatestTransaction(processTransaction, ctx); @@ -855,7 +864,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep boolean isNew = transaction.getId() == null; if (!isNew) { processTransaction = transaction.copyTransactionPropertiesAndMappings(); - ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().put(processTransaction, transaction.getId()); + ctx.getChangedTransactionDetail().addTransactionChange(new TransactionChangeData(transaction, processTransaction)); } processTransaction.setOverPayments(overpayment = MathUtil.minus(overpayment, processAmount)); overpaymentHolder.setMoneyObject(ctxOverpayment = MathUtil.minus(ctxOverpayment, processAmount)); @@ -891,9 +900,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private LoanTransaction checkRegisteredNewTransaction(LoanTransaction newTransaction, TransactionCtx ctx) { ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail(); - Long oldTransactionId = changedTransactionDetail.getCurrentTransactionToOldId().get(newTransaction); - if (oldTransactionId != null) { - LoanTransaction oldTransaction = newTransaction.getLoan().getLoanTransaction(e -> oldTransactionId.equals(e.getId())); + Optional transactionChange = changedTransactionDetail.getTransactionChanges().stream() + .filter(change -> change.getNewTransaction().equals(newTransaction)).findFirst(); + + if (transactionChange.isPresent()) { + LoanTransaction oldTransaction = transactionChange.get().getOldTransaction(); LoanTransaction applicableTransaction = useOldTransactionIfApplicable(oldTransaction, newTransaction, ctx); if (applicableTransaction != null) { return applicableTransaction; @@ -911,9 +922,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep newTransaction.copyLoanTransactionRelations(oldTransaction.getLoanTransactionRelations()); - ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail(); - changedTransactionDetail.getNewTransactionMappings().put(oldTransaction.getId(), newTransaction); - changedTransactionDetail.getCurrentTransactionToOldId().put(newTransaction, oldTransaction.getId()); + ctx.getChangedTransactionDetail().addTransactionChange(new TransactionChangeData(oldTransaction, newTransaction)); return newTransaction; } @@ -922,13 +931,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep TransactionCtx ctx) { MonetaryCurrency currency = ctx.getCurrency(); ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail(); - Map newTransactionMappings = changedTransactionDetail.getNewTransactionMappings(); /* * Check if the transaction amounts have changed or was there any transaction for the same date which was * reverse-replayed. If so, reverse the original transaction and update changedTransactionDetail accordingly to * keep the original order of the transactions. */ - boolean alreadyProcessed = newTransactionMappings.values().stream() + boolean alreadyProcessed = changedTransactionDetail.getTransactionChanges().stream().map(TransactionChangeData::getNewTransaction) .anyMatch(lt -> !lt.equals(newTransaction) && lt.getTransactionDate().equals(oldTransaction.getTransactionDate())); boolean amountMatch = LoanTransaction.transactionAmountsMatch(currency, oldTransaction, newTransaction); if (!alreadyProcessed && amountMatch) { @@ -937,8 +945,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep .updateLoanTransactionToRepaymentScheduleMappings(newTransaction.getLoanTransactionToRepaymentScheduleMappings()); oldTransaction.updateLoanChargePaidMappings(newTransaction.getLoanChargesPaid()); } - changedTransactionDetail.getCurrentTransactionToOldId().remove(newTransaction); - newTransactionMappings.remove(oldTransaction.getId()); + changedTransactionDetail.removeTransactionChange(newTransaction); return oldTransaction; } return null; @@ -956,8 +963,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep .forEach(newRelation -> oldTransaction.getLoanTransactionRelations().stream() .filter(oldRelation -> LoanTransactionRelationTypeEnum.RELATED.equals(oldRelation.getRelationType())) .findFirst().map(oldRelation -> oldRelation.getToTransaction().getId()) - .ifPresent(oldToTransactionId -> newRelation.setToTransaction( - ctx.getChangedTransactionDetail().getNewTransactionMappings().get(oldToTransactionId)))); + .flatMap(oldToTransactionId -> ctx.getChangedTransactionDetail().getTransactionChanges().stream() + .filter(change -> change.getOldTransaction().getId() != null + && change.getOldTransaction().getId().equals(oldToTransactionId)) + .map(TransactionChangeData::getNewTransaction).findFirst()) + .ifPresent(newRelation::setToTransaction)); } // Adding Replayed relation from newly created transaction to reversed transaction @@ -1355,8 +1365,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep final BigDecimal newInterest = getInterestTillChargeOffForPeriod(loanTransaction.getLoan(), loanTransaction.getTransactionDate(), transactionCtx); - createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction.getLoan(), - loanTransaction.getTransactionDate()); + createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction, loanTransaction.getTransactionDate(), + transactionCtx); loanTransaction.resetDerivedComponents(); // determine how much is outstanding total and breakdown for principal, interest and charges @@ -2390,8 +2400,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep return interestTillChargeOff; } - private void createMissingAccrualTransactionDuringChargeOffIfNeeded(final BigDecimal newInterest, final Loan loan, - final LocalDate chargeOffDate) { + private void createMissingAccrualTransactionDuringChargeOffIfNeeded(final BigDecimal newInterest, + final LoanTransaction chargeOffTransaction, final LocalDate chargeOffDate, final TransactionCtx ctx) { + final Loan loan = chargeOffTransaction.getLoan(); final List relevantInstallments = loan.getRepaymentScheduleInstallments().stream() .filter(i -> !i.getFromDate().isAfter(chargeOffDate)).toList(); @@ -2400,10 +2411,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } final BigDecimal sumOfAccrualsTillChargeOff = loan.getLoanTransactions().stream() - .filter(lt -> lt.isAccrual() && !lt.getTransactionDate().isAfter(chargeOffDate)) + .filter(lt -> lt.isAccrual() && !lt.getTransactionDate().isAfter(chargeOffDate) && lt.isNotReversed()) .map(lt -> Optional.ofNullable(lt.getInterestPortion()).orElse(BigDecimal.ZERO)).reduce(BigDecimal.ZERO, BigDecimal::add); - final BigDecimal missingAccrualAmount = newInterest.subtract(sumOfAccrualsTillChargeOff); + final BigDecimal sumOfAccrualAdjustmentsTillChargeOff = loan.getLoanTransactions().stream() + .filter(lt -> lt.isAccrualAdjustment() && !lt.getTransactionDate().isAfter(chargeOffDate) && lt.isNotReversed()) + .map(lt -> Optional.ofNullable(lt.getInterestPortion()).orElse(BigDecimal.ZERO)).reduce(BigDecimal.ZERO, BigDecimal::add); + + final BigDecimal missingAccrualAmount = newInterest.subtract(sumOfAccrualsTillChargeOff).add(sumOfAccrualAdjustmentsTillChargeOff); if (missingAccrualAmount.compareTo(BigDecimal.ZERO) == 0) { return; @@ -2419,13 +2434,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep missingAccrualAmount.abs(), ZERO, ZERO, externalIdFactory.create()); } - loan.addLoanTransaction(newAccrualTransaction); - loanTransactionRepository.saveAndFlush(newAccrualTransaction); - - if (newAccrualTransaction.isAccrual()) { - businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(newAccrualTransaction)); - } else { - businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualAdjustmentTransactionBusinessEvent(newAccrualTransaction)); - } + ctx.getChangedTransactionDetail().addNewTransactionChangeBeforeExistingOne(new TransactionChangeData(null, newAccrualTransaction), + chargeOffTransaction); } } diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index 16bc868be..f6552d7c1 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -53,6 +53,7 @@ import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +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.LoanCharge; @@ -111,7 +112,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest { @BeforeEach public void setUp() { - underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null, null, null, null); + underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null, null); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); @@ -658,11 +659,14 @@ class AdvancedPaymentScheduleTransactionProcessorTest { when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation)); - TransactionCtx ctx = mock(TransactionCtx.class); + Loan loan = mock(Loan.class); + when(chargebackReplayed.getLoan()).thenReturn(loan); + when(loan.getLoanTransactions()).thenReturn(List.of(repayment1, repayment2)); + TransactionChangeData transactionChange = new TransactionChangeData(originalChargeback, chargebackReplayed); ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class); + when(changedTransactionDetail.getTransactionChanges()).thenReturn(List.of(transactionChange)); + TransactionCtx ctx = mock(TransactionCtx.class); when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail); - when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L)); - when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of(122L, repayment1, 121L, repayment2)); // when LoanTransaction originalTransaction = underTest.findChargebackOriginalTransaction(chargebackReplayed, ctx); @@ -692,8 +696,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest { TransactionCtx ctx = mock(TransactionCtx.class); ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class); when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail); - when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L)); - when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of()); + when(changedTransactionDetail.getTransactionChanges()) + .thenReturn(List.of(new TransactionChangeData(originalChargeback, chargebackReplayed))); // when LoanTransaction originalTransaction = underTest.findChargebackOriginalTransaction(chargebackReplayed, ctx); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index f12731bc9..dc689e91b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -2917,14 +2917,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null); loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } + final List loanTransactions = loan.retrieveListOfTransactionsForReprocessing(); + loanTransactions.add(chargeOffTransaction); + reprocessLoanTransactionsService.reprocessTransactions(loan, loanTransactions); loan.addLoanTransaction(chargeOffTransaction); - reprocessLoanTransactionsService.reprocessTransactions(loan); } else { + reprocessLoanTransactionsService.processLatestTransaction(chargeOffTransaction, loan); loan.addLoanTransaction(chargeOffTransaction); - loan.getTransactionProcessor().processLatestTransaction(chargeOffTransaction, - new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), - new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); - loan.updateLoanSummaryDerivedFields(); } loanTransactionRepository.saveAndFlush(chargeOffTransaction); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java index 5079af324..062d0b4b4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java @@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; +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.LoanRepaymentScheduleInstallment; @@ -92,7 +93,9 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS Pair reprocessResult = processor .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess, loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges()); - loan.getLoanTransactions().addAll(reprocessResult.getLeft().getCurrentTransactionToOldId().keySet()); + final List newTransactions = reprocessResult.getLeft().getTransactionChanges().stream() + .map(TransactionChangeData::getNewTransaction).toList(); + loan.getLoanTransactions().addAll(newTransactions); ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight(); return emiCalculator.getSumOfDueInterestsOnDate(modelAfter, relatedRefundTransactionDate); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java index e539e945e..e1c9e0c45 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java @@ -23,6 +23,7 @@ import java.time.LocalDate; import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Objects; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; @@ -97,11 +98,11 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); ProgressiveLoanInterestScheduleModel model = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getRight(); - if (!changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft().getCurrentTransactionToOldId().isEmpty() - || !changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft().getNewTransactionMappings() - .isEmpty()) { - List replayedTransactions = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft() - .getNewTransactionMappings().keySet().stream().toList(); + final List replayedTransactions = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft() + .getTransactionChanges().stream().filter(change -> change.getOldTransaction() != null) + .map(change -> change.getNewTransaction().getId()).filter(Objects::nonNull).toList(); + + if (!replayedTransactions.isEmpty()) { log.warn("Reprocessed transactions show differences: There are unsaved changes of the following transactions: {}", replayedTransactions); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java index 900daa6af..8737fb81d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java @@ -20,10 +20,8 @@ package org.apache.fineract.portfolio.loanaccount.starter; import java.util.List; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; -import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor; @@ -112,10 +110,8 @@ public class LoanAccountAutoStarter { @Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class) public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator, LoanRepositoryWrapper loanRepositoryWrapper, - @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, - LoanTransactionRepository loanTransactionRepository, ExternalIdFactory externalIdFactory, - @Lazy BusinessEventNotifierService businessEventNotifierService) { + @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, ExternalIdFactory externalIdFactory) { return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService, - loanTransactionRepository, externalIdFactory, businessEventNotifierService); + externalIdFactory); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceIntegrationTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceIntegrationTest.java index 951704180..de3f6f556 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceIntegrationTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceIntegrationTest.java @@ -27,6 +27,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.Optional; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; @@ -88,8 +89,9 @@ class ReplayedTransactionBusinessEventServiceIntegrationTest { LoanTransaction oldLoanTransaction = Mockito.mock(LoanTransaction.class); LoanTransaction newLoanTransaction = Mockito.mock(LoanTransaction.class); lenient().when(loanTransactionRepository.findById(1L)).thenReturn(Optional.of(oldLoanTransaction)); + lenient().when(oldLoanTransaction.getId()).thenReturn(1L); ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); - changedTransactionDetail.getNewTransactionMappings().put(1L, newLoanTransaction); + changedTransactionDetail.addTransactionChange(new TransactionChangeData(oldLoanTransaction, newLoanTransaction)); // when underTest.raiseTransactionReplayedEvents(changedTransactionDetail); // then @@ -103,13 +105,16 @@ class ReplayedTransactionBusinessEventServiceIntegrationTest { @Test public void testWhenParamHasTwoNewTransaction() { // given - LoanTransaction oldLoanTransaction = Mockito.mock(LoanTransaction.class); + LoanTransaction oldLoanTransaction1 = Mockito.mock(LoanTransaction.class); + LoanTransaction oldLoanTransaction2 = Mockito.mock(LoanTransaction.class); LoanTransaction newLoanTransaction = Mockito.mock(LoanTransaction.class); - lenient().when(loanTransactionRepository.findById(1L)).thenReturn(Optional.of(oldLoanTransaction)); - lenient().when(loanTransactionRepository.findById(2L)).thenReturn(Optional.of(oldLoanTransaction)); + lenient().when(loanTransactionRepository.findById(1L)).thenReturn(Optional.of(oldLoanTransaction1)); + lenient().when(loanTransactionRepository.findById(2L)).thenReturn(Optional.of(oldLoanTransaction2)); + lenient().when(oldLoanTransaction1.getId()).thenReturn(1L); + lenient().when(oldLoanTransaction2.getId()).thenReturn(2L); ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); - changedTransactionDetail.getNewTransactionMappings().put(1L, newLoanTransaction); - changedTransactionDetail.getNewTransactionMappings().put(2L, newLoanTransaction); + changedTransactionDetail.addTransactionChange(new TransactionChangeData(oldLoanTransaction1, newLoanTransaction)); + changedTransactionDetail.addTransactionChange(new TransactionChangeData(oldLoanTransaction2, newLoanTransaction)); // when underTest.raiseTransactionReplayedEvents(changedTransactionDetail); // then @@ -128,8 +133,9 @@ class ReplayedTransactionBusinessEventServiceIntegrationTest { LoanTransaction oldLoanTransaction = Mockito.mock(LoanTransaction.class); LoanTransaction newLoanTransaction = Mockito.mock(LoanTransaction.class); lenient().when(loanTransactionRepository.findById(1L)).thenReturn(Optional.of(oldLoanTransaction)); + lenient().when(oldLoanTransaction.getId()).thenReturn(1L); ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); - changedTransactionDetail.getNewTransactionMappings().put(1L, newLoanTransaction); + changedTransactionDetail.addTransactionChange(new TransactionChangeData(oldLoanTransaction, newLoanTransaction)); // when assertThrows(RuntimeException.class, () -> underTest.raiseTransactionReplayedEvents(changedTransactionDetail)); // then