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


The following commit(s) were added to refs/heads/develop by this push:
     new 22282ded00 FINERACT-2181: Rework 
"createMissingAccrualTransactionDuringChargeOffIfNeeded" to avoid flushing and 
triggering business event as part of Transaction processor
22282ded00 is described below

commit 22282ded00d382d8fac54ab79893a404efada379
Author: mariiaKraievska <[email protected]>
AuthorDate: Mon Mar 10 12:50:03 2025 +0200

    FINERACT-2181: Rework 
"createMissingAccrualTransactionDuringChargeOffIfNeeded" to avoid flushing and 
triggering business event as part of Transaction processor
---
 .../AbstractAuditableWithUTCDateTimeCustom.java    |   4 +-
 .../TransactionChangeData.java}                    |  19 +--
 .../domain/ChangedTransactionDetail.java           |  44 ++++++-
 .../portfolio/loanaccount/domain/Loan.java         |  17 ++-
 ...tLoanRepaymentScheduleTransactionProcessor.java |  27 +++--
 .../LoanRepaymentScheduleTransactionProcessor.java |   4 +-
 ...eplayedTransactionBusinessEventServiceImpl.java |  31 +++--
 .../service/ReprocessLoanTransactionsService.java  |   6 +
 .../ReprocessLoanTransactionsServiceImpl.java      |  48 +++++++-
 ...dvancedPaymentScheduleTransactionProcessor.java | 127 +++++++++++----------
 ...cedPaymentScheduleTransactionProcessorTest.java |  16 ++-
 .../LoanWritePlatformServiceJpaRepositoryImpl.java |   9 +-
 .../ProgressiveLoanInterestRefundServiceImpl.java  |   5 +-
 .../ProgressiveLoanSummaryDataProvider.java        |  11 +-
 .../starter/LoanAccountAutoStarter.java            |   8 +-
 ...sactionBusinessEventServiceIntegrationTest.java |  20 ++--
 16 files changed, 260 insertions(+), 136 deletions(-)

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 9798cbf8ea..6612602bfd 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<T extends Serializa
 
     private static final long serialVersionUID = 141481953116476081L;
 
-    @Column(name = CREATED_BY_DB_FIELD, nullable = false)
+    @Column(name = CREATED_BY_DB_FIELD, updatable = false, nullable = false)
     @Setter(onMethod = @__(@Override))
     private Long createdBy;
 
-    @Column(name = CREATED_DATE_DB_FIELD, nullable = false)
+    @Column(name = CREATED_DATE_DB_FIELD, updatable = false, nullable = false)
     @Setter(onMethod = @__(@Override))
     private OffsetDateTime createdDate;
 
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/TransactionChangeData.java
similarity index 65%
copy from 
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
copy to 
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/TransactionChangeData.java
index 79e6e74d54..66b29c0a2e 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/TransactionChangeData.java
@@ -16,20 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.portfolio.loanaccount.domain;
+package org.apache.fineract.portfolio.loanaccount.data;
 
-import java.util.LinkedHashMap;
-import java.util.Map;
+import lombok.AllArgsConstructor;
 import lombok.Getter;
+import lombok.Setter;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
 
 /**
- * Stores details of {@link LoanTransaction}'s that were reversed or newly 
created
+ * Represents a transaction change, storing both the old reversed transaction 
(if any) and the new transaction.
  */
 @Getter
-public class ChangedTransactionDetail {
-
-    private final Map<Long, LoanTransaction> newTransactionMappings = new 
LinkedHashMap<>();
-
-    private final Map<LoanTransaction, Long> currentTransactionToOldId = new 
LinkedHashMap<>();
+@Setter
+@AllArgsConstructor
+public class TransactionChangeData {
 
+    private LoanTransaction oldTransaction;
+    private LoanTransaction newTransaction;
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
index 79e6e74d54..ad4245fc0e 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
@@ -18,9 +18,12 @@
  */
 package org.apache.fineract.portfolio.loanaccount.domain;
 
-import java.util.LinkedHashMap;
-import java.util.Map;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
 import lombok.Getter;
+import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
 
 /**
  * Stores details of {@link LoanTransaction}'s that were reversed or newly 
created
@@ -28,8 +31,41 @@ import lombok.Getter;
 @Getter
 public class ChangedTransactionDetail {
 
-    private final Map<Long, LoanTransaction> newTransactionMappings = new 
LinkedHashMap<>();
+    private final List<TransactionChangeData> transactionChanges = new 
ArrayList<>();
 
-    private final Map<LoanTransaction, Long> 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<TransactionChangeData> 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 feccefffca..240bb33bbb 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<Long> {
         ChangedTransactionDetail changedTransactionDetail = 
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
                 getDisbursementDate(), 
allNonContraTransactionsPostDisbursement, getCurrency(), 
getRepaymentScheduleInstallments(),
                 getActiveCharges());
-        for (final Map.Entry<Long, LoanTransaction> 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<LoanTransaction> newTransactions = 
changedTransactionDetail.getTransactionChanges().stream()
+                .map(TransactionChangeData::getNewTransaction).toList();
+        this.loanTransactions.addAll(newTransactions);
         updateLoanSummaryDerivedFields();
         return changedTransactionDetail;
     }
@@ -2656,14 +2659,16 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom<Long> {
         ChangedTransactionDetail changedTransactionDetail = 
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
                 getDisbursementDate(), 
allNonContraTransactionsPostDisbursement, getCurrency(), 
getRepaymentScheduleInstallments(),
                 getActiveCharges());
-        for (final Map.Entry<Long, LoanTransaction> 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<LoanTransaction> 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 722328cd25..765df9f396 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<LoanTransaction> transactionsToBeProcessed) {
-
         List<LoanTransaction> mergedTransactionList = 
getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail);
-        for (Map.Entry<Long, LoanTransaction> 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<LoanTransaction> 
getMergedTransactionList(List<LoanTransaction> transactionList,
             ChangedTransactionDetail changedTransactionDetail) {
-        List<LoanTransaction> mergedList = new 
ArrayList<>(changedTransactionDetail.getNewTransactionMappings().values());
-        mergedList.addAll(new ArrayList<>(transactionList));
+        List<LoanTransaction> 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 f49635f629..411f26309d 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 6ac3ca3d18..f31cbe6923 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<Long, LoanTransaction> 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 bc84ad8048..ed11c5ee7f 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<LoanTransaction> 
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 d29867e529..334fedbc1c 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<LoanTransaction> 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<LoanTransaction> 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<LoanTransaction> 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<Long, LoanTransaction> 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 14dde4885b..6d25be7b39 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<Long, LoanTransaction> 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<TransactionChangeData> 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<LoanTransaction> 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<LoanRepaymentScheduleInstallment> 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<TransactionChangeData> 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<LoanTransaction> updatedTransactions = 
changedTransactionDetail.getNewTransactionMappings().values();
-            Optional<LoanTransaction> fromTransaction = 
updatedTransactions.stream()
+            Optional<LoanTransaction> 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<TransactionChangeData> transactionChanges = 
changedTransactionDetail.getTransactionChanges();
+
+            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();
 
-        // 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));
+                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<TransactionChangeData> 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<Long, LoanTransaction> 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<LoanRepaymentScheduleInstallment> 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 16bc868be6..f6552d7c16 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 f12731bc93..dc689e91ba 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<LoanTransaction> 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 5079af3242..062d0b4b4f 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<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> 
reprocessResult = processor
                 
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), 
relatedRefundTransactionDate, transactionsToReprocess,
                         loan.getCurrency(), installmentsToReprocess, 
loan.getActiveCharges());
-        
loan.getLoanTransactions().addAll(reprocessResult.getLeft().getCurrentTransactionToOldId().keySet());
+        final List<LoanTransaction> 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 e539e945e0..e1c9e0c45f 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<Long> replayedTransactions = 
changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft()
-                            
.getNewTransactionMappings().keySet().stream().toList();
+                final List<Long> 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 900daa6afc..8737fb81d8 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 9517041801..de3f6f5565 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

Reply via email to