You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2022/11/29 09:04:29 UTC

[fineract] branch develop updated: FINERACT-1760: Raise transaction replayed business event

This is an automated email from the ASF dual-hosted git repository.

arnold 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 4c597b61f FINERACT-1760: Raise transaction replayed business event
4c597b61f is described below

commit 4c597b61fb6f460ff0755aa899f278f395c481f6
Author: Adam Saghy <ad...@gmail.com>
AuthorDate: Mon Nov 28 21:24:57 2022 +0100

    FINERACT-1760: Raise transaction replayed business event
---
 .../fineract/cob/COBBusinessStepServiceImpl.java   |  31 +++--
 .../service/BusinessEventNotifierService.java      |   2 +
 .../service/BusinessEventNotifierServiceImpl.java  |   6 +
 .../domain/LoanAccountDomainServiceJpa.java        |   7 ++
 ...nRescheduleRequestWritePlatformServiceImpl.java |  46 +------
 .../LoanChargeWritePlatformServiceImpl.java        |   7 ++
 .../LoanWritePlatformServiceJpaRepositoryImpl.java |  16 +++
 .../ReplayedTransactionBusinessEventService.java   |  26 ++++
 ...eplayedTransactionBusinessEventServiceImpl.java |  59 +++++++++
 .../cob/COBBusinessStepServiceStepDefinitions.java |  10 ++
 .../InitialisationTaskletStepDefinitions.java      |   4 +
 ...sactionBusinessEventServiceIntegrationTest.java | 140 +++++++++++++++++++++
 .../src/test/resources/features/cob/cob.feature    |   8 ++
 13 files changed, 310 insertions(+), 52 deletions(-)

diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java
index e716c1ac3..2a62c5099 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java
@@ -51,20 +51,27 @@ public class COBBusinessStepServiceImpl implements COBBusinessStepService {
         if (executionMap == null || executionMap.isEmpty()) {
             throw new BusinessStepException("Execution map is empty! COB Business step execution skipped!");
         }
-        businessEventNotifierService.startExternalEventRecording();
-        for (String businessStep : executionMap.values()) {
-            try {
-                ThreadLocalContextUtil.setActionContext(ActionContext.COB);
-                COBBusinessStep<S> businessStepBean = (COBBusinessStep<S>) applicationContext.getBean(businessStep);
-                item = businessStepBean.execute(item);
-            } catch (Exception e) {
-                throw new BusinessStepException("Error happened during business step execution", e);
-            } finally {
-                // Fallback to COB action context after each business step
-                ThreadLocalContextUtil.setActionContext(ActionContext.COB);
+        // Extra safety net to avoid event leaking
+        try {
+            businessEventNotifierService.startExternalEventRecording();
+
+            for (String businessStep : executionMap.values()) {
+                try {
+                    ThreadLocalContextUtil.setActionContext(ActionContext.COB);
+                    COBBusinessStep<S> businessStepBean = (COBBusinessStep<S>) applicationContext.getBean(businessStep);
+                    item = businessStepBean.execute(item);
+                } catch (Exception e) {
+                    throw new BusinessStepException("Error happened during business step execution", e);
+                } finally {
+                    // Fallback to COB action context after each business step
+                    ThreadLocalContextUtil.setActionContext(ActionContext.COB);
+                }
             }
+            businessEventNotifierService.stopExternalEventRecording();
+        } catch (Exception e) {
+            businessEventNotifierService.resetEventRecording();
+            throw e;
         }
-        businessEventNotifierService.stopExternalEventRecording();
         return item;
     }
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java
index d666b0eb6..d0176bc41 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java
@@ -50,4 +50,6 @@ public interface BusinessEventNotifierService {
     void startExternalEventRecording();
 
     void stopExternalEventRecording();
+
+    void resetEventRecording();
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
index e1369b22f..dca732ad9 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
@@ -155,4 +155,10 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe
             recordedEvents.remove();
         }
     }
+
+    @Override
+    public void resetEventRecording() {
+        eventRecordingEnabled.set(false);
+        recordedEvents.remove();
+    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
index 6652bcb1f..0846bbcaa 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
@@ -91,6 +91,7 @@ import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
 import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
 import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
 import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
+import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
 import org.apache.fineract.portfolio.note.domain.Note;
 import org.apache.fineract.portfolio.note.domain.NoteRepository;
 import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
@@ -127,6 +128,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
     private final DelinquencyWritePlatformService delinquencyWritePlatformService;
     private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
     private final ExternalIdFactory externalIdFactory;
+    private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
 
     @Transactional
     @Override
@@ -210,11 +212,14 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
 
         if (changedTransactionDetail != null) {
             for (Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
+
                 saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
                 // update loan with references to the newly created transactions
                 loan.addLoanTransaction(mapEntry.getValue());
                 updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
             }
+            // Trigger transaction replayed event
+            replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
         }
 
         if (StringUtils.isNotBlank(noteText)) {
@@ -788,6 +793,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
                 loan.getLoanTransactions().add(mapEntry.getValue());
                 updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
             }
+            // Trigger transaction replayed event
+            replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
         }
 
         saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java
index 1a2244346..6396908d0 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java
@@ -30,6 +30,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import lombok.RequiredArgsConstructor;
 import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
 import org.apache.fineract.infrastructure.codes.domain.CodeValue;
 import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
@@ -78,10 +79,10 @@ import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanResch
 import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRescheduleRequestNotFoundException;
 import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
 import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
+import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
 import org.apache.fineract.useradministration.domain.AppUser;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.dao.NonTransientDataAccessException;
 import org.springframework.orm.jpa.JpaSystemException;
@@ -89,6 +90,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 @Service
+@RequiredArgsConstructor
 public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanRescheduleRequestWritePlatformService {
 
     private static final Logger LOG = LoggerFactory.getLogger(LoanRescheduleRequestWritePlatformServiceImpl.class);
@@ -111,45 +113,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
     private static final DefaultScheduledDateGenerator DEFAULT_SCHEDULED_DATE_GENERATOR = new DefaultScheduledDateGenerator();
     private final LoanAccountDomainService loanAccountDomainService;
     private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository;
-
-    /**
-     * LoanRescheduleRequestWritePlatformServiceImpl constructor
-     *
-     *
-     **/
-    @Autowired
-    public LoanRescheduleRequestWritePlatformServiceImpl(final CodeValueRepositoryWrapper codeValueRepositoryWrapper,
-            final PlatformSecurityContext platformSecurityContext,
-            final LoanRescheduleRequestDataValidator loanRescheduleRequestDataValidator,
-            final LoanRescheduleRequestRepository loanRescheduleRequestRepository,
-            final LoanRepaymentScheduleHistoryRepository loanRepaymentScheduleHistoryRepository,
-            final LoanScheduleHistoryWritePlatformService loanScheduleHistoryWritePlatformService,
-            final LoanTransactionRepository loanTransactionRepository,
-            final JournalEntryWritePlatformService journalEntryWritePlatformService, final LoanRepositoryWrapper loanRepositoryWrapper,
-            final LoanAssembler loanAssembler, final LoanUtilService loanUtilService,
-            final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
-            final LoanScheduleGeneratorFactory loanScheduleFactory, final LoanSummaryWrapper loanSummaryWrapper,
-            final AccountTransfersWritePlatformService accountTransfersWritePlatformService,
-            final LoanAccountDomainService loanAccountDomainService,
-            final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository) {
-        this.codeValueRepositoryWrapper = codeValueRepositoryWrapper;
-        this.platformSecurityContext = platformSecurityContext;
-        this.loanRescheduleRequestDataValidator = loanRescheduleRequestDataValidator;
-        this.loanRescheduleRequestRepository = loanRescheduleRequestRepository;
-        this.loanRepaymentScheduleHistoryRepository = loanRepaymentScheduleHistoryRepository;
-        this.loanScheduleHistoryWritePlatformService = loanScheduleHistoryWritePlatformService;
-        this.loanTransactionRepository = loanTransactionRepository;
-        this.journalEntryWritePlatformService = journalEntryWritePlatformService;
-        this.loanRepositoryWrapper = loanRepositoryWrapper;
-        this.loanAssembler = loanAssembler;
-        this.loanUtilService = loanUtilService;
-        this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory;
-        this.loanScheduleFactory = loanScheduleFactory;
-        this.loanSummaryWrapper = loanSummaryWrapper;
-        this.accountTransfersWritePlatformService = accountTransfersWritePlatformService;
-        this.loanAccountDomainService = loanAccountDomainService;
-        this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository;
-    }
+    private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
 
     /**
      * create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist
@@ -490,6 +454,8 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
                     loan.addLoanTransaction(mapEntry.getValue());
                     this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
                 }
+                // Trigger transaction replayed event
+                replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
             }
             postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
index c187d86dd..745d76901 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
@@ -155,6 +155,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
     private final ExternalIdFactory externalIdFactory;
     private final AccountTransferDetailRepository accountTransferDetailRepository;
     private final LoanChargeAssembler loanChargeAssembler;
+    private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
 
     private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
         return e.getFromDate().isBefore(loanCharge.getDueDate()) && !loanCharge.getDueDate().isAfter(e.getDueDate());
@@ -246,6 +247,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
                     loan.addLoanTransaction(mapEntry.getValue());
                     this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
                 }
+                // Trigger transaction replayed event
+                replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
             }
             this.loanRepositoryWrapper.save(loan);
         }
@@ -740,6 +743,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
                         loan.addLoanTransaction(mapEntry.getValue());
                         this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
                     }
+                    // Trigger transaction replayed event
+                    replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
                 }
             }
 
@@ -1085,6 +1090,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
                     loan.addLoanTransaction(mapEntry.getValue());
                     this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
                 }
+                // Trigger transaction replayed event
+                replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
             }
         }
     }
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 19d680f0a..7cb647553 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
@@ -241,6 +241,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
     private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
     private final LoanAccountLockService loanAccountLockService;
     private final ExternalIdFactory externalIdFactory;
+    private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
 
     @Transactional
     @Override
@@ -457,6 +458,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
                     this.loanTransactionRepository.save(mapEntry.getValue());
                     this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
                 }
+                // Trigger transaction replayed event
+                replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
             }
 
             // auto create standing instruction
@@ -732,6 +735,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
                         this.loanTransactionRepository.save(mapEntry.getValue());
                         this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
                     }
+                    // Trigger transaction replayed event
+                    replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
                 }
                 postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
             }
@@ -1129,6 +1134,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
                 loan.addLoanTransaction(mapEntry.getValue());
                 this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
             }
+            // Trigger transaction replayed event
+            replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
         }
 
         final String noteText = command.stringValueOfParameterNamed("note");
@@ -1360,6 +1367,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
                 loan.addLoanTransaction(mapEntry.getValue());
                 this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
             }
+            // Trigger transaction replayed event
+            replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
         }
 
         final String noteText = command.stringValueOfParameterNamed("note");
@@ -2128,6 +2137,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
                 this.loanTransactionRepository.save(mapEntry.getValue());
                 this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
             }
+            // Trigger transaction replayed event
+            replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
         }
         saveLoanWithDataIntegrityViolationChecks(loan);
 
@@ -2268,8 +2279,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
 
         if (command.entityId() != null && changedTransactionDetail != null) {
             for (Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
+                this.loanTransactionRepository.save(mapEntry.getValue());
                 updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
             }
+            // Trigger transaction replayed event
+            replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
         }
         if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
             createLoanScheduleArchive(loan, scheduleGeneratorDTO);
@@ -2326,6 +2340,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
                 loan.addLoanTransaction(mapEntry.getValue());
                 this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
             }
+            // Trigger transaction replayed event
+            replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
         }
         postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
         loanAccountDomainService.recalculateAccruals(loan);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventService.java
new file mode 100644
index 000000000..614c9b719
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventService.java
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.service;
+
+import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
+
+public interface ReplayedTransactionBusinessEventService {
+
+    void raiseTransactionReplayedEvents(ChangedTransactionDetail changedTransactionDetail);
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java
new file mode 100644
index 000000000..1359e00e6
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+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.service.BusinessEventNotifierService;
+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;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class ReplayedTransactionBusinessEventServiceImpl implements ReplayedTransactionBusinessEventService {
+
+    private final BusinessEventNotifierService businessEventNotifierService;
+    private final LoanTransactionRepository loanTransactionRepository;
+
+    @Override
+    public void raiseTransactionReplayedEvents(final ChangedTransactionDetail changedTransactionDetail) {
+        if (changedTransactionDetail == null || changedTransactionDetail.getNewTransactionMappings().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));
+            }
+            businessEventNotifierService.stopExternalEventRecording();
+        } catch (Exception e) {
+            businessEventNotifierService.resetEventRecording();
+            throw e;
+        }
+    }
+}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java
index 866e13511..88cf32de9 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 import com.google.common.base.Splitter;
 import io.cucumber.java8.En;
@@ -39,6 +40,7 @@ import org.apache.fineract.infrastructure.core.domain.ActionContext;
 import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
 import org.apache.fineract.mix.data.MixTaxonomyData;
+import org.mockito.Mockito;
 import org.springframework.beans.factory.BeanCreationException;
 import org.springframework.beans.factory.ListableBeanFactory;
 import org.springframework.context.ApplicationContext;
@@ -147,6 +149,14 @@ public class COBBusinessStepServiceStepDefinitions implements En {
             ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
         });
 
+        Then("throw exception COBBusinessStepService.run method with verification", () -> {
+            assertThrows(BusinessStepException.class, () -> {
+                resultItem = this.businessStepService.run(this.executionMap, this.item);
+            });
+            verify(businessEventNotifierService, Mockito.times(1)).resetEventRecording();
+            ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+        });
+
         Then("The COBBusinessStepService.getCOBBusinessStepMap result exception", () -> {
             assertThrows(BeanCreationException.class, () -> {
                 this.businessStepService.getCOBBusinessStepMap(this.clazz, this.jobName);
diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java
index 435266e52..cf4a41fe5 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java
@@ -24,6 +24,8 @@ import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.mock;
 
 import io.cucumber.java8.En;
+import org.apache.fineract.infrastructure.core.domain.ActionContext;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import org.apache.fineract.useradministration.domain.AppUser;
 import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
 import org.springframework.batch.repeat.RepeatStatus;
@@ -56,12 +58,14 @@ public class InitialisationTaskletStepDefinitions implements En {
         Then("InitialisationTasklet.execute result should match", () -> {
             assertEquals(RepeatStatus.FINISHED, resultItem);
             assertEquals(appUser, SecurityContextHolder.getContext().getAuthentication().getPrincipal());
+            ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
         });
 
         Then("throw exception InitialisationTasklet.execute method", () -> {
             assertThrows(RuntimeException.class, () -> {
                 resultItem = this.initialisationTasklet.execute(null, null);
             });
+            ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
         });
     }
 }
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
new file mode 100644
index 000000000..824f22c15
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceIntegrationTest.java
@@ -0,0 +1,140 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.service;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+
+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.domain.ChangedTransactionDetail;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class ReplayedTransactionBusinessEventServiceIntegrationTest {
+
+    @Mock
+    private BusinessEventNotifierService businessEventNotifierService;
+
+    @Mock
+    private LoanTransactionRepository loanTransactionRepository;
+
+    private ReplayedTransactionBusinessEventService underTest;
+
+    @BeforeEach
+    public void setUp() {
+        underTest = new ReplayedTransactionBusinessEventServiceImpl(businessEventNotifierService, loanTransactionRepository);
+    }
+
+    @Test
+    public void testWhenParamIsNull() {
+        // given
+        ChangedTransactionDetail changedTransactionDetail = null;
+        // when
+        underTest.raiseTransactionReplayedEvents(changedTransactionDetail);
+        // then
+        verify(businessEventNotifierService, Mockito.times(0)).startExternalEventRecording();
+        verify(businessEventNotifierService, Mockito.times(0))
+                .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class));
+        verify(businessEventNotifierService, Mockito.times(0)).stopExternalEventRecording();
+        verify(businessEventNotifierService, Mockito.times(0)).resetEventRecording();
+    }
+
+    @Test
+    public void testWhenParamHasNoMapping() {
+        // given
+        ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
+        // when
+        underTest.raiseTransactionReplayedEvents(changedTransactionDetail);
+        // then
+        verify(businessEventNotifierService, Mockito.times(0)).startExternalEventRecording();
+        verify(businessEventNotifierService, Mockito.times(0))
+                .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class));
+        verify(businessEventNotifierService, Mockito.times(0)).stopExternalEventRecording();
+        verify(businessEventNotifierService, Mockito.times(0)).resetEventRecording();
+    }
+
+    @Test
+    public void testWhenParamHasOneNewTransaction() {
+        // given
+        LoanTransaction oldLoanTransaction = Mockito.mock(LoanTransaction.class);
+        LoanTransaction newLoanTransaction = Mockito.mock(LoanTransaction.class);
+        lenient().when(loanTransactionRepository.findById(1L)).thenReturn(Optional.of(oldLoanTransaction));
+        ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
+        changedTransactionDetail.getNewTransactionMappings().put(1L, newLoanTransaction);
+        // when
+        underTest.raiseTransactionReplayedEvents(changedTransactionDetail);
+        // then
+        verify(businessEventNotifierService, Mockito.times(1)).startExternalEventRecording();
+        verify(businessEventNotifierService, Mockito.times(1))
+                .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class));
+        verify(businessEventNotifierService, Mockito.times(1)).stopExternalEventRecording();
+        verify(businessEventNotifierService, Mockito.times(0)).resetEventRecording();
+    }
+
+    @Test
+    public void testWhenParamHasTwoNewTransaction() {
+        // given
+        LoanTransaction oldLoanTransaction = 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));
+        ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
+        changedTransactionDetail.getNewTransactionMappings().put(1L, newLoanTransaction);
+        changedTransactionDetail.getNewTransactionMappings().put(2L, newLoanTransaction);
+        // when
+        underTest.raiseTransactionReplayedEvents(changedTransactionDetail);
+        // then
+        verify(businessEventNotifierService, Mockito.times(1)).startExternalEventRecording();
+        verify(businessEventNotifierService, Mockito.times(2))
+                .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class));
+        verify(businessEventNotifierService, Mockito.times(1)).stopExternalEventRecording();
+        verify(businessEventNotifierService, Mockito.times(0)).resetEventRecording();
+    }
+
+    @Test
+    public void testWhenParamHasError() {
+        // given
+        doThrow(new RuntimeException()).when(businessEventNotifierService)
+                .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class));
+        LoanTransaction oldLoanTransaction = Mockito.mock(LoanTransaction.class);
+        LoanTransaction newLoanTransaction = Mockito.mock(LoanTransaction.class);
+        lenient().when(loanTransactionRepository.findById(1L)).thenReturn(Optional.of(oldLoanTransaction));
+        ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
+        changedTransactionDetail.getNewTransactionMappings().put(1L, newLoanTransaction);
+        // when
+        assertThrows(RuntimeException.class, () -> underTest.raiseTransactionReplayedEvents(changedTransactionDetail));
+        // then
+        verify(businessEventNotifierService, Mockito.times(1)).startExternalEventRecording();
+        verify(businessEventNotifierService, Mockito.times(1))
+                .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class));
+        verify(businessEventNotifierService, Mockito.times(0)).stopExternalEventRecording();
+        verify(businessEventNotifierService, Mockito.times(1)).resetEventRecording();
+    }
+}
diff --git a/fineract-provider/src/test/resources/features/cob/cob.feature b/fineract-provider/src/test/resources/features/cob/cob.feature
index 4818ee6d9..5645e271e 100644
--- a/fineract-provider/src/test/resources/features/cob/cob.feature
+++ b/fineract-provider/src/test/resources/features/cob/cob.feature
@@ -37,6 +37,14 @@ Feature: COB
     Examples:
       |executionMap|
       |null|
+
+  @cob
+  Scenario Outline: COB Business Step Service - run test failure
+    Given The COBBusinessStepService.run method with executeMap <executionMap>
+    Then throw exception COBBusinessStepService.run method with verification
+
+    Examples:
+      |executionMap|
       |2,notExist|
       |3,|