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 141e26fda FINERACT-1694: Upgrade External Business Event Handling
141e26fda is described below

commit 141e26fda7abafabd2eff5e9d23f49c2030990ec
Author: Soma Sörös <[email protected]>
AuthorDate: Wed Jul 31 09:33:15 2024 +0200

    FINERACT-1694: Upgrade External Business Event Handling
---
 .../service/BusinessEventNotifierServiceImpl.java  |  74 ++++++++++-
 .../event/business/service/TransactionHelper.java  |  33 +++++
 .../BusinessEventNotifierServiceImplTest.java      | 137 +++++++++++++++++++++
 .../CustomSnapshotEventIntegrationTest.java        |  18 ++-
 4 files changed, 255 insertions(+), 7 deletions(-)

diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
index 9809f6342..4543539d1 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
@@ -22,9 +22,14 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Stack;
+import lombok.Getter;
 import lombok.RequiredArgsConstructor;
+import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.domain.FineractContext;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
 import 
org.apache.fineract.infrastructure.event.business.domain.BulkBusinessEvent;
 import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
@@ -33,12 +38,16 @@ import 
org.apache.fineract.infrastructure.event.external.repository.ExternalEven
 import 
org.apache.fineract.infrastructure.event.external.service.ExternalEventService;
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.TransactionExecution;
+import org.springframework.transaction.TransactionExecutionListener;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
 
 @Service
 @SuppressWarnings({ "unchecked", "rawtypes" })
 @RequiredArgsConstructor
 @Slf4j
-public class BusinessEventNotifierServiceImpl implements 
BusinessEventNotifierService, InitializingBean {
+public class BusinessEventNotifierServiceImpl implements 
BusinessEventNotifierService, InitializingBean, TransactionExecutionListener {
 
     private final Map<Class, List<BusinessEventListener>> preListeners = new 
HashMap<>();
     private final Map<Class, List<BusinessEventListener>> postListeners = new 
HashMap<>();
@@ -49,6 +58,8 @@ public class BusinessEventNotifierServiceImpl implements 
BusinessEventNotifierSe
     private final ExternalEventService externalEventService;
     private final ExternalEventConfigurationRepository 
eventConfigurationRepository;
     private final FineractProperties fineractProperties;
+    private final ThreadLocal<Stack<List<BusinessEventWithContext>>> 
transactionBusinessEvents = ThreadLocal.withInitial(Stack::new);
+    private final TransactionHelper transactionHelper;
 
     @Override
     public void afterPropertiesSet() throws Exception {
@@ -79,6 +90,7 @@ public class BusinessEventNotifierServiceImpl implements 
BusinessEventNotifierSe
     }
 
     @Override
+    @Transactional(propagation = Propagation.SUPPORTS)
     public void notifyPostBusinessEvent(BusinessEvent<?> businessEvent) {
         throwExceptionIfBulkEvent(businessEvent);
         boolean isExternalEvent = !(businessEvent instanceof NoExternalEvent);
@@ -92,7 +104,11 @@ public class BusinessEventNotifierServiceImpl implements 
BusinessEventNotifierSe
                 if (isExternalEventRecordingEnabled()) {
                     recordedEvents.get().add(businessEvent);
                 } else {
-                    externalEventService.postEvent(businessEvent);
+                    if (transactionHelper.hasTransaction()) {
+                        storeTransactionalBusinessEvent(businessEvent);
+                    } else {
+                        externalEventService.postEvent(businessEvent);
+                    }
                 }
             }
         }
@@ -170,4 +186,58 @@ public class BusinessEventNotifierServiceImpl implements 
BusinessEventNotifierSe
         eventRecordingEnabled.set(false);
         recordedEvents.remove();
     }
+
+    private void storeTransactionalBusinessEvent(BusinessEvent<?> 
businessEvent) {
+        List<BusinessEventWithContext> businessEvents = 
transactionBusinessEvents.get().peek();
+        FineractContext fineractContext = ThreadLocalContextUtil.getContext();
+        businessEvents.add(new BusinessEventWithContext(businessEvent, 
fineractContext));
+    }
+
+    private void cleanup() {
+        transactionBusinessEvents.get().pop();
+    }
+
+    @Override
+    public void afterBegin(TransactionExecution transaction, Throwable 
beginFailure) {
+        transactionBusinessEvents.get().push(new ArrayList<>());
+    }
+
+    @Override
+    public void beforeCommit(TransactionExecution transaction) {
+        List<BusinessEventWithContext> businessEventWithContexts = 
transactionBusinessEvents.get().peek();
+        if (!businessEventWithContexts.isEmpty()) {
+            FineractContext originalContext = 
ThreadLocalContextUtil.getContext();
+            try {
+                for (BusinessEventWithContext businessEventWithContext : 
businessEventWithContexts) {
+                    
ThreadLocalContextUtil.init(businessEventWithContext.getFineractContext());
+                    
externalEventService.postEvent(businessEventWithContext.getEvent());
+                }
+            } finally {
+                ThreadLocalContextUtil.init(originalContext);
+            }
+        }
+    }
+
+    @Override
+    public void afterCommit(TransactionExecution transaction, Throwable 
commitFailure) {
+        cleanup();
+    }
+
+    @Override
+    public void afterRollback(TransactionExecution transaction, Throwable 
rollbackFailure) {
+        cleanup();
+    }
+
+    @Getter
+    @Setter
+    private static final class BusinessEventWithContext {
+
+        private BusinessEvent<?> event;
+        private FineractContext fineractContext;
+
+        BusinessEventWithContext(BusinessEvent<?> event, FineractContext 
fineractContext) {
+            this.event = event;
+            this.fineractContext = fineractContext;
+        }
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/TransactionHelper.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/TransactionHelper.java
new file mode 100644
index 000000000..7a30304e0
--- /dev/null
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/TransactionHelper.java
@@ -0,0 +1,33 @@
+/**
+ * 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.infrastructure.event.business.service;
+
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.interceptor.TransactionAspectSupport;
+
+@Component
+@Transactional(propagation = Propagation.SUPPORTS)
+public class TransactionHelper {
+
+    boolean hasTransaction() {
+        return 
TransactionAspectSupport.currentTransactionStatus().hasTransaction();
+    }
+}
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java
index f69be4296..73d0ee2b9 100644
--- 
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java
@@ -27,7 +27,11 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
+import java.time.LocalDate;
+import java.util.HashMap;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
 import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
 import 
org.apache.fineract.infrastructure.event.business.domain.BulkBusinessEvent;
 import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
@@ -43,6 +47,7 @@ import org.mockito.Mockito;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoSettings;
 import org.mockito.quality.Strictness;
+import org.springframework.transaction.TransactionExecution;
 
 @ExtendWith(MockitoExtension.class)
 @SuppressWarnings({ "rawtypes", "unchecked" })
@@ -58,9 +63,140 @@ class BusinessEventNotifierServiceImplTest {
     @Mock
     private FineractProperties fineractProperties;
 
+    @Mock
+    private TransactionHelper transactionHelper;
+
     @InjectMocks
     private BusinessEventNotifierServiceImpl underTest;
 
+    @Test
+    public void 
testNotifyPostBusinessEventShouldCollectEventsWithinTransaction() {
+        // given
+        setBusinessDate();
+        configureExternalEventsProperties(true);
+        MockBusinessEvent event = new MockBusinessEvent();
+        BusinessEventListener<MockBusinessEvent> postListener = mockListener();
+        underTest.addPostBusinessEventListener(MockBusinessEvent.class, 
postListener);
+        TransactionExecution mockTransaction = 
mock(TransactionExecution.class);
+        underTest.afterBegin(mockTransaction, null);
+        // when
+        
when(externalEventConfigurationRepository.findExternalEventConfigurationByTypeWithNotFoundDetection(Mockito.any()))
+                .thenReturn(new ExternalEventConfiguration("aType", true));
+        when(transactionHelper.hasTransaction()).thenReturn(true);
+        underTest.notifyPostBusinessEvent(event);
+        // then
+        verify(postListener).onBusinessEvent(event);
+        verifyNoInteractions(externalEventService);
+        // simulate finish transaction
+        underTest.beforeCommit(mockTransaction);
+        verify(externalEventService).postEvent(event);
+        underTest.afterCommit(mockTransaction, null);
+        verifyNoInteractions(mockTransaction);
+    }
+
+    private void setBusinessDate() {
+        HashMap<BusinessDateType, LocalDate> map = new HashMap<>(2);
+        map.put(BusinessDateType.BUSINESS_DATE, LocalDate.of(2023, 2, 1));
+        map.put(BusinessDateType.COB_DATE, LocalDate.of(2023, 1, 31));
+        ThreadLocalContextUtil.setBusinessDates(map);
+    }
+
+    @Test
+    public void 
testNotifyPostBusinessEventShouldCollectEventsWithinTransactionInNestedTransaction()
 {
+        // given
+        setBusinessDate();
+        configureExternalEventsProperties(true);
+        MockBusinessEvent event = new MockBusinessEvent();
+        MockBusinessEvent nestedEvent = new MockBusinessEvent();
+        BusinessEventListener<MockBusinessEvent> postListener = mockListener();
+        underTest.addPostBusinessEventListener(MockBusinessEvent.class, 
postListener);
+        TransactionExecution mockTransaction = 
mock(TransactionExecution.class);
+        // when
+        
when(externalEventConfigurationRepository.findExternalEventConfigurationByTypeWithNotFoundDetection(Mockito.any()))
+                .thenReturn(new ExternalEventConfiguration("aType", true));
+        when(transactionHelper.hasTransaction()).thenReturn(true);
+
+        // simulate outer transaction
+        underTest.afterBegin(mockTransaction, null);
+        underTest.notifyPostBusinessEvent(event);
+        verify(postListener).onBusinessEvent(event);
+        verifyNoInteractions(externalEventService);
+        // simulate nested transaction
+        underTest.afterBegin(mockTransaction, null);
+        underTest.notifyPostBusinessEvent(nestedEvent);
+        verify(postListener).onBusinessEvent(nestedEvent);
+        verifyNoInteractions(externalEventService);
+        // simulate commit nested transaction
+        underTest.beforeCommit(mockTransaction);
+        underTest.afterCommit(mockTransaction, null);
+        verify(externalEventService).postEvent(nestedEvent);
+        // simulate commit outer transaction
+        underTest.beforeCommit(mockTransaction);
+        verify(externalEventService).postEvent(event);
+        underTest.afterCommit(mockTransaction, null);
+        verifyNoInteractions(mockTransaction);
+    }
+
+    @Test
+    public void 
testNotifyPostBusinessEventShouldCollectEventsWithinTransactionInNestedRollbackTransaction()
 {
+        // given
+        setBusinessDate();
+        configureExternalEventsProperties(true);
+        MockBusinessEvent event = new MockBusinessEvent();
+        MockBusinessEvent nestedEvent = new MockBusinessEvent();
+        BusinessEventListener<MockBusinessEvent> postListener = mockListener();
+        underTest.addPostBusinessEventListener(MockBusinessEvent.class, 
postListener);
+        TransactionExecution mockTransaction = 
mock(TransactionExecution.class);
+        // when
+        
when(externalEventConfigurationRepository.findExternalEventConfigurationByTypeWithNotFoundDetection(Mockito.any()))
+                .thenReturn(new ExternalEventConfiguration("aType", true));
+        when(transactionHelper.hasTransaction()).thenReturn(true);
+
+        // simulate outer transaction
+        underTest.afterBegin(mockTransaction, null);
+        underTest.notifyPostBusinessEvent(event);
+        verify(postListener).onBusinessEvent(event);
+        verifyNoInteractions(externalEventService);
+        // simulate nested transaction
+        underTest.afterBegin(mockTransaction, null);
+        underTest.notifyPostBusinessEvent(nestedEvent);
+        verify(postListener).onBusinessEvent(nestedEvent);
+        verifyNoInteractions(externalEventService);
+        // simulate commit nested transaction
+        underTest.afterRollback(mockTransaction, null);
+        verifyNoInteractions(externalEventService);
+        // simulate commit outer transaction
+        underTest.beforeCommit(mockTransaction);
+        verify(externalEventService).postEvent(event);
+        underTest.afterCommit(mockTransaction, null);
+        verifyNoInteractions(mockTransaction);
+    }
+
+    @Test
+    public void 
testNotifyPostBusinessEventShouldCollectEventsWithinTransactionAndNotSendExternalOnRollback()
 {
+        // given
+        setBusinessDate();
+        configureExternalEventsProperties(true);
+        MockBusinessEvent event = new MockBusinessEvent();
+        BusinessEventListener<MockBusinessEvent> postListener = mockListener();
+        underTest.addPostBusinessEventListener(MockBusinessEvent.class, 
postListener);
+        TransactionExecution mockTransaction = 
mock(TransactionExecution.class);
+        underTest.afterBegin(mockTransaction, null);
+        // when
+        
when(externalEventConfigurationRepository.findExternalEventConfigurationByTypeWithNotFoundDetection(Mockito.any()))
+                .thenReturn(new ExternalEventConfiguration("aType", true));
+        when(transactionHelper.hasTransaction()).thenReturn(true);
+        underTest.notifyPostBusinessEvent(event);
+        // then
+        verify(postListener).onBusinessEvent(event);
+        verifyNoInteractions(externalEventService);
+        // simulate rollback transaction
+        verifyNoInteractions(externalEventService);
+        underTest.afterRollback(mockTransaction, null);
+        verifyNoInteractions(externalEventService);
+        verifyNoInteractions(mockTransaction);
+    }
+
     @Test
     public void testNotifyPostBusinessEventShouldNotifyPostListeners() {
         // given
@@ -85,6 +221,7 @@ class BusinessEventNotifierServiceImplTest {
         BusinessEventListener<MockBusinessEvent> postListener = mockListener();
         underTest.addPostBusinessEventListener(MockBusinessEvent.class, 
postListener);
 
+        when(transactionHelper.hasTransaction()).thenReturn(false);
         
when(externalEventConfigurationRepository.findExternalEventConfigurationByTypeWithNotFoundDetection(Mockito.any()))
                 .thenReturn(new ExternalEventConfiguration("aType", true));
         // when
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
index 98f9ab0b1..abb535e37 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
@@ -46,8 +46,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 @ExtendWith({ LoanTestLifecycleExtension.class, ExternalEventsExtension.class 
})
 public class CustomSnapshotEventIntegrationTest extends 
BaseLoanIntegrationTest {
 
-    private SchedulerJobHelper schedulerJobHelper = new 
SchedulerJobHelper(this.requestSpec);
-    private Gson gson = new Gson();
+    private final SchedulerJobHelper schedulerJobHelper = new 
SchedulerJobHelper(this.requestSpec);
 
     @Test
     public void testSnapshotEventGenerationWhenLoanInstallmentIsNotPayed() {
@@ -221,9 +220,8 @@ public class CustomSnapshotEventIntegrationTest extends 
BaseLoanIntegrationTest
     public void 
testNoSnapshotEventGenerationWhenCOBDateIsNotMatchingWithInstallmentDueDate() {
         runAt("30 January 2023", () -> {
             // Enable Business Step
-            enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", 
"LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
-                    "CHECK_LOAN_REPAYMENT_OVERDUE", 
"UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
-                    "EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS");
+            enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", 
"LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_OVERDUE",
+                    "UPDATE_LOAN_ARREARS_AGING", 
"ADD_PERIODIC_ACCRUAL_ENTRIES", "EXTERNAL_ASSET_OWNER_TRANSFER", 
"CHECK_DUE_INSTALLMENTS");
 
             enableLoanAccountCustomSnapshotBusinessEvent();
 
@@ -265,6 +263,8 @@ public class CustomSnapshotEventIntegrationTest extends 
BaseLoanIntegrationTest
     @Test
     public void 
testNoSnapshotEventGenerationWhenCustomSnapshotEventIsDisabled() {
         runAt("31 January 2023", () -> {
+            // disable custom snapshot event
+            disableLoanAccountCustomSnapshotBusinessEvent();
             // Enable Business Step
             enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", 
"LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
                     "CHECK_LOAN_REPAYMENT_OVERDUE", 
"UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
@@ -333,6 +333,14 @@ public class CustomSnapshotEventIntegrationTest extends 
BaseLoanIntegrationTest
         
Assertions.assertTrue(updatedConfigurations.get("LoanAccountCustomSnapshotBusinessEvent"));
     }
 
+    private void disableLoanAccountCustomSnapshotBusinessEvent() {
+        final Map<String, Boolean> updatedConfigurations = 
ExternalEventConfigurationHelper.updateExternalEventConfigurations(requestSpec,
+                responseSpec, 
"{\"externalEventConfigurations\":{\"LoanAccountCustomSnapshotBusinessEvent\":false}}\n");
+        Assertions.assertEquals(updatedConfigurations.size(), 1);
+        
Assertions.assertTrue(updatedConfigurations.containsKey("LoanAccountCustomSnapshotBusinessEvent"));
+        
Assertions.assertFalse(updatedConfigurations.get("LoanAccountCustomSnapshotBusinessEvent"));
+    }
+
     private void updateBusinessDateAndExecuteCOBJob(String date) {
         businessDateHelper.updateBusinessDate(
                 new 
BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));

Reply via email to