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/09/02 08:03:19 UTC

[fineract] 03/03: FINERACT-1694: External event storage integration

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

commit f46abaf9e288f88d85f3959018cf2baa30d252cd
Author: Arnold Galovics <ga...@gmail.com>
AuthorDate: Thu Sep 1 17:57:59 2022 +0200

    FINERACT-1694: External event storage integration
---
 build.gradle                                       |   3 +-
 .../fixeddeposit/v1/FixedDepositAccountDataV1.avsc |   3 +
 .../src/main/avro/generic/v1/CalendarDataV1.avsc   |   2 +
 .../src/main/avro/group/v1/GroupGeneralDataV1.avsc |   2 +
 .../main/avro/loan/v1/DelinquencyBucketDataV1.avsc |   1 +
 .../src/main/avro/loan/v1/LoanAccountDataV1.avsc   |   7 ++
 .../src/main/avro/loan/v1/LoanChargeDataV1.avsc    |   1 +
 .../src/main/avro/loan/v1/LoanProductDataV1.avsc   |   5 +
 .../main/avro/loan/v1/LoanTransactionDataV1.avsc   |   1 +
 .../src/main/avro/office/v1/OfficeDataV1.avsc      |   1 +
 .../v1/RecurringDepositAccountDataV1.avsc          |   2 +
 .../main/avro/savings/v1/SavingsAccountDataV1.avsc |   3 +
 .../v1/SavingsAccountTransactionDataV1.avsc        |   1 +
 .../src/main/avro/share/v1/ShareAccountDataV1.avsc |   2 +
 .../src/main/avro/share/v1/ShareProductDataV1.avsc |   2 +
 .../external/repository/domain/ExternalEvent.java  |   2 +-
 .../service/DelayedExternalEventService.java       |   8 ++
 .../BusinessEventSerializerComparator.java         |  43 --------
 .../BusinessEventSerializerFactory.java            |   6 +-
 .../SavingsAccountTransactionDataMapper.java       |  29 ++++++
 ...anAdjustTransactionBusinessEventSerializer.java |  13 ++-
 ...sAccountTransactionBusinessEventSerializer.java |  58 +++++++++++
 .../service/LoanReadPlatformServiceImpl.java       |  11 +-
 .../portfolio/savings/data/SavingsAccountData.java |   1 -
 .../0045_external_event_table_data_binary.xml      |   5 +-
 .../service/DelayedExternalEventServiceTest.java   | 116 +++++++++++++++++++++
 .../external/service/ExternalEventServiceTest.java | 115 ++++++++++++++++++++
 .../src/test/resources/application-test.properties |   1 +
 integration-tests/build.gradle                     |   2 +-
 29 files changed, 386 insertions(+), 60 deletions(-)

diff --git a/build.gradle b/build.gradle
index 7fe39014a..68b87f05f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -279,6 +279,8 @@ configure(project.fineractJavaProjects) {
     sourceSets.main.output.resourcesDir = sourceSets.main.java.outputDir
     sourceSets.test.output.resourcesDir = sourceSets.test.java.outputDir
 
+    check.dependsOn('cucumber')
+
     configurations {
         implementation.setCanBeResolved(true)
         api.setCanBeResolved(true)
@@ -536,7 +538,6 @@ configure(project.fineractJavaProjects) {
 
     test {
         useJUnitPlatform()
-        dependsOn 'cucumber'
 
         if (project.hasProperty('excludeTests')) {
             filter {
diff --git a/fineract-avro-schemas/src/main/avro/fixeddeposit/v1/FixedDepositAccountDataV1.avsc b/fineract-avro-schemas/src/main/avro/fixeddeposit/v1/FixedDepositAccountDataV1.avsc
index 09971d770..ea91d36ee 100644
--- a/fineract-avro-schemas/src/main/avro/fixeddeposit/v1/FixedDepositAccountDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/fixeddeposit/v1/FixedDepositAccountDataV1.avsc
@@ -84,6 +84,7 @@
             ]
         },
         {
+            "default": null,
             "name": "fieldOfficerName",
             "type": [
                 "null",
@@ -219,6 +220,7 @@
             ]
         },
         {
+            "default": null,
             "name": "transactions",
             "type": [
                 "null",
@@ -229,6 +231,7 @@
             ]
         },
         {
+            "default": null,
             "name": "charges",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/generic/v1/CalendarDataV1.avsc b/fineract-avro-schemas/src/main/avro/generic/v1/CalendarDataV1.avsc
index 9aa8df4b4..388f482ee 100644
--- a/fineract-avro-schemas/src/main/avro/generic/v1/CalendarDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/generic/v1/CalendarDataV1.avsc
@@ -172,6 +172,7 @@
             ]
         },
         {
+            "default": null,
             "name": "recurringDates",
             "type": [
                 "null",
@@ -182,6 +183,7 @@
             ]
         },
         {
+            "default": null,
             "name": "nextTenRecurringDates",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/group/v1/GroupGeneralDataV1.avsc b/fineract-avro-schemas/src/main/avro/group/v1/GroupGeneralDataV1.avsc
index 2f51475b0..4f6094a2c 100644
--- a/fineract-avro-schemas/src/main/avro/group/v1/GroupGeneralDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/group/v1/GroupGeneralDataV1.avsc
@@ -124,6 +124,7 @@
             ]
         },
         {
+            "default": null,
             "name": "groupRoles",
             "type": [
                 "null",
@@ -134,6 +135,7 @@
             ]
         },
         {
+            "default": null,
             "name": "calendarsData",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/DelinquencyBucketDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/DelinquencyBucketDataV1.avsc
index 20dae32c3..2a60d7088 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/DelinquencyBucketDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/DelinquencyBucketDataV1.avsc
@@ -20,6 +20,7 @@
             ]
         },
         {
+            "default": null,
             "name": "ranges",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
index 3ea653e6f..a3b18cf00 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
@@ -436,6 +436,7 @@
             ]
         },
         {
+            "default": null,
             "name": "transactions",
             "type": [
                 "null",
@@ -446,6 +447,7 @@
             ]
         },
         {
+            "default": null,
             "name": "charges",
             "type": [
                 "null",
@@ -464,6 +466,7 @@
             ]
         },
         {
+            "default": null,
             "name": "disbursementDetails",
             "type": [
                 "null",
@@ -514,6 +517,7 @@
             ]
         },
         {
+            "default": null,
             "name": "accountLinkingOptions",
             "type": [
                 "null",
@@ -564,6 +568,7 @@
             ]
         },
         {
+            "default": null,
             "name": "emiAmountVariations",
             "type": [
                 "null",
@@ -574,6 +579,7 @@
             ]
         },
         {
+            "default": null,
             "name": "clientActiveLoanOptions",
             "type": [
                 "null",
@@ -640,6 +646,7 @@
             ]
         },
         {
+            "default": null,
             "name": "overdueCharges",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc
index 6e0f6cb7c..a11601ef7 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc
@@ -124,6 +124,7 @@
             ]
         },
         {
+            "default": null,
             "name": "chargeOptions",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductDataV1.avsc
index 3e6770fa8..8576f4c20 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductDataV1.avsc
@@ -460,6 +460,7 @@
             ]
         },
         {
+            "default": null,
             "name": "charges",
             "type": [
                 "null",
@@ -470,6 +471,7 @@
             ]
         },
         {
+            "default": null,
             "name": "principalVariationsForBorrowerCycle",
             "type": [
                 "null",
@@ -480,6 +482,7 @@
             ]
         },
         {
+            "default": null,
             "name": "interestRateVariationsForBorrowerCycle",
             "type": [
                 "null",
@@ -490,6 +493,7 @@
             ]
         },
         {
+            "default": null,
             "name": "numberOfRepaymentVariationsForBorrowerCycle",
             "type": [
                 "null",
@@ -508,6 +512,7 @@
             ]
         },
         {
+            "default": null,
             "name": "rates",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc
index 216d75b6e..1ea5c2a4a 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc
@@ -180,6 +180,7 @@
             ]
         },
         {
+            "default": null,
             "name": "loanChargePaidByList",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/office/v1/OfficeDataV1.avsc b/fineract-avro-schemas/src/main/avro/office/v1/OfficeDataV1.avsc
index b93be4215..d7117c21e 100644
--- a/fineract-avro-schemas/src/main/avro/office/v1/OfficeDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/office/v1/OfficeDataV1.avsc
@@ -68,6 +68,7 @@
             ]
         },
         {
+            "default": null,
             "name": "allowedParents",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/recurringdeposit/v1/RecurringDepositAccountDataV1.avsc b/fineract-avro-schemas/src/main/avro/recurringdeposit/v1/RecurringDepositAccountDataV1.avsc
index 1c4b2b4e4..11cf3b264 100644
--- a/fineract-avro-schemas/src/main/avro/recurringdeposit/v1/RecurringDepositAccountDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/recurringdeposit/v1/RecurringDepositAccountDataV1.avsc
@@ -220,6 +220,7 @@
             ]
         },
         {
+            "default": null,
             "name": "transactions",
             "type": [
                 "null",
@@ -230,6 +231,7 @@
             ]
         },
         {
+            "default": null,
             "name": "charges",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountDataV1.avsc b/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountDataV1.avsc
index 62f68c180..558f13764 100644
--- a/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountDataV1.avsc
@@ -92,6 +92,7 @@
             ]
         },
         {
+            "default": null,
             "name": "fieldOfficerName",
             "type": [
                 "null",
@@ -339,6 +340,7 @@
             ]
         },
         {
+            "default": null,
             "name": "transactions",
             "type": [
                 "null",
@@ -349,6 +351,7 @@
             ]
         },
         {
+            "default": null,
             "name": "charges",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountTransactionDataV1.avsc b/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountTransactionDataV1.avsc
index 7385ddc89..e2cbbe34f 100644
--- a/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountTransactionDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountTransactionDataV1.avsc
@@ -180,6 +180,7 @@
             ]
         },
         {
+            "default": null,
             "name": "chargesPaidByData",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/share/v1/ShareAccountDataV1.avsc b/fineract-avro-schemas/src/main/avro/share/v1/ShareAccountDataV1.avsc
index 9dcb79362..7f998e596 100644
--- a/fineract-avro-schemas/src/main/avro/share/v1/ShareAccountDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/share/v1/ShareAccountDataV1.avsc
@@ -108,6 +108,7 @@
             ]
         },
         {
+            "default": null,
             "name": "purchasedShares",
             "type": [
                 "null",
@@ -118,6 +119,7 @@
             ]
         },
         {
+            "default": null,
             "name": "savingsAccountId",
             "type": [
                 "null",
diff --git a/fineract-avro-schemas/src/main/avro/share/v1/ShareProductDataV1.avsc b/fineract-avro-schemas/src/main/avro/share/v1/ShareProductDataV1.avsc
index 1aa53bc27..6ddf1e8da 100644
--- a/fineract-avro-schemas/src/main/avro/share/v1/ShareProductDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/share/v1/ShareProductDataV1.avsc
@@ -108,6 +108,7 @@
             ]
         },
         {
+            "default": null,
             "name": "marketPrice",
             "type": [
                 "null",
@@ -118,6 +119,7 @@
             ]
         },
         {
+            "default": null,
             "name": "charges",
             "type": [
                 "null",
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/repository/domain/ExternalEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/repository/domain/ExternalEvent.java
index 73933d6cb..961d51cbc 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/repository/domain/ExternalEvent.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/repository/domain/ExternalEvent.java
@@ -54,7 +54,7 @@ public class ExternalEvent extends AbstractPersistableCustom {
     @Setter
     private ExternalEventStatus status;
 
-    @Column(name = "sent_at", nullable = false)
+    @Column(name = "sent_at", nullable = true)
     @Setter
     private OffsetDateTime sentAt;
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventService.java
index b415a0010..f043a9f84 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventService.java
@@ -45,6 +45,14 @@ public class DelayedExternalEventService {
         return !localEventStorage.get().isEmpty();
     }
 
+    public void clearEnqueuedEvents() {
+        localEventStorage.get().clear();
+    }
+
+    public List<BusinessEvent<?>> getEnqueuedEvents() {
+        return List.copyOf(localEventStorage.get());
+    }
+
     public void postEnqueuedEvents() {
         List<BusinessEvent<?>> enqueuedEvents = localEventStorage.get();
         if (enqueuedEvents.isEmpty()) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/BusinessEventSerializerComparator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/BusinessEventSerializerComparator.java
deleted file mode 100644
index 6cff1b1d5..000000000
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/BusinessEventSerializerComparator.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * 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.external.service.serialization;
-
-import java.util.Comparator;
-import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer;
-import org.springframework.core.Ordered;
-import org.springframework.core.annotation.Order;
-
-public class BusinessEventSerializerComparator implements Comparator<BusinessEventSerializer> {
-
-    @Override
-    public int compare(BusinessEventSerializer o1, BusinessEventSerializer o2) {
-        int o1Order = getOrderOrDefault(o1);
-        int o2Order = getOrderOrDefault(o2);
-        return Integer.compare(o1Order, o2Order);
-    }
-
-    private int getOrderOrDefault(BusinessEventSerializer serializer) {
-        Order orderAnnotation = serializer.getClass().getAnnotation(Order.class);
-        if (orderAnnotation != null) {
-            return orderAnnotation.value();
-        } else {
-            return Ordered.LOWEST_PRECEDENCE;
-        }
-    }
-}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/BusinessEventSerializerFactory.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/BusinessEventSerializerFactory.java
index 86421db0d..7046a56bd 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/BusinessEventSerializerFactory.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/BusinessEventSerializerFactory.java
@@ -19,19 +19,17 @@
 package org.apache.fineract.infrastructure.event.external.service.serialization;
 
 import java.util.List;
+import lombok.RequiredArgsConstructor;
 import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer;
 import org.apache.fineract.portfolio.businessevent.domain.BusinessEvent;
 import org.springframework.stereotype.Component;
 
 @Component
+@RequiredArgsConstructor
 public class BusinessEventSerializerFactory {
 
     private final List<BusinessEventSerializer> serializers;
 
-    public BusinessEventSerializerFactory(List<BusinessEventSerializer> serializers) {
-        this.serializers = serializers.stream().sorted(new BusinessEventSerializerComparator()).toList();
-    }
-
     public <T> BusinessEventSerializer create(BusinessEvent<T> event) {
         for (BusinessEventSerializer serializer : serializers) {
             if (serializer.canSerialize(event)) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/savings/SavingsAccountTransactionDataMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/savings/SavingsAccountTransactionDataMapper.java
new file mode 100644
index 000000000..dabf8bc7e
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/savings/SavingsAccountTransactionDataMapper.java
@@ -0,0 +1,29 @@
+/**
+ * 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.external.service.serialization.mapper.savings;
+
+import org.apache.fineract.avro.savings.v1.SavingsAccountTransactionDataV1;
+import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
+import org.mapstruct.Mapper;
+
+@Mapper(componentModel = "spring")
+public interface SavingsAccountTransactionDataMapper {
+
+    SavingsAccountTransactionDataV1 map(SavingsAccountTransactionData source);
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAdjustTransactionBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAdjustTransactionBusinessEventSerializer.java
index 62d163d7a..179b8c555 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAdjustTransactionBusinessEventSerializer.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAdjustTransactionBusinessEventSerializer.java
@@ -50,13 +50,18 @@ public class LoanAdjustTransactionBusinessEventSerializer implements BusinessEve
     public <T> byte[] serialize(BusinessEvent<T> rawEvent) throws IOException {
         LoanAdjustTransactionBusinessEvent event = (LoanAdjustTransactionBusinessEvent) rawEvent;
         LoanTransaction transactionToAdjust = event.get().getTransactionToAdjust();
-        LoanTransaction newTransactionDetail = event.get().getNewTransactionDetail();
         LoanTransactionData transactionToAdjustData = service.retrieveLoanTransaction(transactionToAdjust.getLoan().getId(),
                 transactionToAdjust.getId());
-        LoanTransactionData newTransactionDetailData = service.retrieveLoanTransaction(newTransactionDetail.getLoan().getId(),
-                newTransactionDetail.getId());
         LoanTransactionDataV1 transactionToAdjustAvroDto = mapper.map(transactionToAdjustData);
-        LoanTransactionDataV1 newTransactionDetailAvroDto = mapper.map(newTransactionDetailData);
+
+        LoanTransaction newTransactionDetail = event.get().getNewTransactionDetail();
+        LoanTransactionDataV1 newTransactionDetailAvroDto = null;
+        if (newTransactionDetail != null) {
+            LoanTransactionData newTransactionDetailData = service.retrieveLoanTransaction(newTransactionDetail.getLoan().getId(),
+                    newTransactionDetail.getId());
+            newTransactionDetailAvroDto = mapper.map(newTransactionDetailData);
+
+        }
         LoanTransactionAdjustmentDataV1 avroDto = new LoanTransactionAdjustmentDataV1(transactionToAdjustAvroDto,
                 newTransactionDetailAvroDto);
         ByteBuffer buffer = avroDto.toByteBuffer();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/savings/SavingsAccountTransactionBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/savings/SavingsAccountTransactionBusinessEventSerializer.java
new file mode 100644
index 000000000..487c3d6dd
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/savings/SavingsAccountTransactionBusinessEventSerializer.java
@@ -0,0 +1,58 @@
+/**
+ * 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.external.service.serialization.serializer.savings;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.avro.savings.v1.SavingsAccountTransactionDataV1;
+import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.savings.SavingsAccountTransactionDataMapper;
+import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer;
+import org.apache.fineract.infrastructure.event.external.service.support.ByteBufferConverter;
+import org.apache.fineract.portfolio.businessevent.domain.BusinessEvent;
+import org.apache.fineract.portfolio.businessevent.domain.savings.transaction.SavingsAccountTransactionBusinessEvent;
+import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
+import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class SavingsAccountTransactionBusinessEventSerializer implements BusinessEventSerializer {
+
+    private final SavingsAccountReadPlatformService service;
+    private final SavingsAccountTransactionDataMapper mapper;
+    private final ByteBufferConverter byteBufferConverter;
+
+    @Override
+    public <T> boolean canSerialize(BusinessEvent<T> event) {
+        return event instanceof SavingsAccountTransactionBusinessEvent;
+    }
+
+    @Override
+    public <T> byte[] serialize(BusinessEvent<T> rawEvent) throws IOException {
+        SavingsAccountTransactionBusinessEvent event = (SavingsAccountTransactionBusinessEvent) rawEvent;
+        SavingsAccountTransaction tx = event.get();
+        SavingsAccountTransactionData data = service.retrieveSavingsTransaction(tx.getSavingsAccount().getId(), tx.getId(),
+                tx.getSavingsAccount().depositAccountType());
+        SavingsAccountTransactionDataV1 avroDto = mapper.map(data);
+        ByteBuffer buffer = avroDto.toByteBuffer();
+        return byteBufferConverter.convert(buffer);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
index 11fb67d8d..2eab5338b 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
@@ -212,8 +212,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
     public LoanAccountData retrieveOne(final Long loanId) {
 
         try {
-            final AppUser currentUser = this.context.authenticatedUser();
-            final String hierarchy = currentUser.getOffice().getHierarchy();
+            final String hierarchy = getHierarchyString();
             final String hierarchySearchString = hierarchy + "%";
 
             final LoanMapper rm = new LoanMapper(sqlGenerator, delinquencyReadPlatformService);
@@ -231,6 +230,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
         }
     }
 
+    private String getHierarchyString() {
+        AppUser currentUser = null;
+        if (this.context != null) {
+            currentUser = this.context.getAuthenticatedUserIfPresent();
+        }
+        return Optional.ofNullable(currentUser).map(appUser -> appUser.getOffice().getHierarchy()).orElse(".");
+    }
+
     @Override
     public LoanAccountData retrieveLoanByLoanAccount(String loanAccountNumber) {
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java
index 5eed082b8..0b006421d 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java
@@ -406,7 +406,6 @@ public final class SavingsAccountData implements Serializable {
         return this.interestCompoundingPeriodType;
     }
 
-
     public Integer getInterestCompoundingPeriodTypeId() {
         return this.interestCompoundingPeriodType.getId().intValue();
     }
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0045_external_event_table_data_binary.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0045_external_event_table_data_binary.xml
index 65a5d3629..56cdc032d 100644
--- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0045_external_event_table_data_binary.xml
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0045_external_event_table_data_binary.xml
@@ -24,17 +24,16 @@
                    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
     <changeSet author="fineract" id="1">
         <delete tableName="m_external_event"/>
+        <dropColumn tableName="m_external_event" columnName="data"/>
     </changeSet>
     <changeSet author="fineract" id="2-mysql" context="mysql">
-        <dropColumn tableName="m_external_event" columnName="data"/>
         <addColumn tableName="m_external_event">
-            <column name="data" type="VARBINARY">
+            <column name="data" type="BLOB">
                 <constraints nullable="false"/>
             </column>
         </addColumn>
     </changeSet>
     <changeSet author="fineract" id="2-postgresql" context="postgresql">
-        <dropColumn tableName="m_external_event" columnName="data"/>
         <addColumn tableName="m_external_event">
             <column name="data" type="BYTEA">
                 <constraints nullable="false"/>
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventServiceTest.java
new file mode 100644
index 000000000..6f05a7a6a
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventServiceTest.java
@@ -0,0 +1,116 @@
+/**
+ * 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.external.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.util.List;
+import org.apache.fineract.portfolio.businessevent.domain.BulkBusinessEvent;
+import org.apache.fineract.portfolio.businessevent.domain.BusinessEvent;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+@SuppressWarnings({ "rawtypes", "unchecked" })
+class DelayedExternalEventServiceTest {
+
+    @Mock
+    private ExternalEventService delegate;
+
+    @InjectMocks
+    private DelayedExternalEventService underTest;
+
+    @BeforeEach
+    public void setUp() {
+        underTest.clearEnqueuedEvents();
+    }
+
+    @Test
+    public void testEnqueueEventFailsWhenNullEventIsGiven() {
+        // given
+        // when & then
+        assertThatThrownBy(() -> underTest.enqueueEvent(null)).isExactlyInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void testEnqueueEventWorks() {
+        // given
+        BusinessEvent event = mock(BusinessEvent.class);
+        // when
+        underTest.enqueueEvent(event);
+        // then
+        List<BusinessEvent<?>> enqueuedEvents = underTest.getEnqueuedEvents();
+        assertThat(enqueuedEvents).hasSize(1);
+        assertThat(enqueuedEvents.get(0)).isEqualTo(event);
+    }
+
+    @Test
+    public void testHasEnqueuedEventsReturnsTrueWhenEventIsEnqueued() {
+        // given
+        BusinessEvent event = mock(BusinessEvent.class);
+        underTest.enqueueEvent(event);
+        // when
+        boolean result = underTest.hasEnqueuedEvents();
+        // then
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void testHasEnqueuedEventsReturnsFalseWhenNoEventIsEnqueued() {
+        // given
+        // when
+        boolean result = underTest.hasEnqueuedEvents();
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void testPostEnqueuedEventsFailsWhenNoEventIsEnqueued() {
+        // given
+        // when & then
+        assertThatThrownBy(() -> underTest.postEnqueuedEvents()).isExactlyInstanceOf(IllegalStateException.class);
+    }
+
+    @Test
+    public void testPostEnqueuedEventsWorks() {
+        // given
+        ArgumentCaptor<BusinessEvent> delegateEventCaptor = ArgumentCaptor.forClass(BusinessEvent.class);
+
+        BusinessEvent event = mock(BusinessEvent.class);
+        underTest.enqueueEvent(event);
+        // when
+        underTest.postEnqueuedEvents();
+        // then
+        verify(delegate).postEvent(delegateEventCaptor.capture());
+        BusinessEvent delegateEvent = delegateEventCaptor.getValue();
+        assertThat(delegateEvent).isExactlyInstanceOf(BulkBusinessEvent.class);
+        BulkBusinessEvent bulkDelegateEvent = (BulkBusinessEvent) delegateEvent;
+        List<BusinessEvent<?>> enqueuedEvents = bulkDelegateEvent.get();
+        assertThat(enqueuedEvents).hasSize(1);
+        assertThat(enqueuedEvents.get(0)).isEqualTo(event);
+    }
+}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventServiceTest.java
new file mode 100644
index 000000000..b65c8c907
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventServiceTest.java
@@ -0,0 +1,115 @@
+/**
+ * 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.external.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.infrastructure.event.external.repository.ExternalEventRepository;
+import org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEvent;
+import org.apache.fineract.infrastructure.event.external.service.idempotency.ExternalEventIdempotencyKeyGenerator;
+import org.apache.fineract.infrastructure.event.external.service.serialization.BusinessEventSerializerFactory;
+import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer;
+import org.apache.fineract.portfolio.businessevent.domain.BusinessEvent;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+@SuppressWarnings({ "rawtypes", "unchecked" })
+class ExternalEventServiceTest {
+
+    @Mock
+    private ExternalEventRepository repository;
+    @Mock
+    private ExternalEventIdempotencyKeyGenerator idempotencyKeyGenerator;
+    @Mock
+    private BusinessEventSerializerFactory serializerFactory;
+
+    @InjectMocks
+    private ExternalEventService underTest;
+
+    @BeforeEach
+    public void setUp() {
+        FineractPlatformTenant tenant = new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null);
+        ThreadLocalContextUtil.setTenant(tenant);
+        ThreadLocalContextUtil
+                .setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault()))));
+    }
+
+    @Test
+    public void testPostEventShouldFailWhenNullEventIsGiven() {
+        // given
+        // when & then
+        assertThatThrownBy(() -> underTest.postEvent(null)).isExactlyInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void testPostEventShouldFailWhenEventSerializationFails() throws IOException {
+        // given
+        BusinessEvent event = mock(BusinessEvent.class);
+        BusinessEventSerializer eventSerializer = mock(BusinessEventSerializer.class);
+
+        given(idempotencyKeyGenerator.generate(event)).willReturn("");
+        given(serializerFactory.create(event)).willReturn(eventSerializer);
+        given(eventSerializer.serialize(event)).willThrow(IOException.class);
+        // when & then
+        assertThatThrownBy(() -> underTest.postEvent(event)).isExactlyInstanceOf(RuntimeException.class);
+    }
+
+    @Test
+    public void testPostEventShouldWork() throws IOException {
+        // given
+        ArgumentCaptor<ExternalEvent> externalEventArgumentCaptor = ArgumentCaptor.forClass(ExternalEvent.class);
+
+        String eventType = "TestType";
+        String idempotencyKey = "key";
+        BusinessEvent event = mock(BusinessEvent.class);
+        BusinessEventSerializer eventSerializer = mock(BusinessEventSerializer.class);
+        byte[] data = new byte[0];
+
+        given(event.getType()).willReturn(eventType);
+        given(idempotencyKeyGenerator.generate(event)).willReturn(idempotencyKey);
+        given(serializerFactory.create(event)).willReturn(eventSerializer);
+        given(eventSerializer.serialize(event)).willReturn(data);
+        // when
+        underTest.postEvent(event);
+        // then
+        verify(repository).save(externalEventArgumentCaptor.capture());
+        ExternalEvent externalEvent = externalEventArgumentCaptor.getValue();
+        assertThat(externalEvent.getIdempotencyKey()).isEqualTo(idempotencyKey);
+        assertThat(externalEvent.getData()).isEqualTo(data);
+        assertThat(externalEvent.getType()).isEqualTo(eventType);
+    }
+}
diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties
index 5999ebcc3..425a5d45b 100644
--- a/fineract-provider/src/test/resources/application-test.properties
+++ b/fineract-provider/src/test/resources/application-test.properties
@@ -45,6 +45,7 @@ fineract.partitioned-job.partitioned-job-properties[0].thread-count=1
 fineract.remote-job-message-handler.spring-events.enabled=${FINERACT_REMOTE_JOB_MESSAGE_HANDLER_SPRING_EVENTS_ENABLED:true}
 fineract.remote-job-message-handler.jms.enabled=${FINERACT_REMOTE_JOB_MESSAGE_HANDLER_JMS_ENABLED:false}
 fineract.remote-job-message-handler.jms.request-queue-name=${FINERACT_REMOTE_JOB_MESSAGE_HANDLER_JMS_QUEUE_NAME:JMS-request-queue}
+fineract.events.external.enabled=${FINERACT_EXTERNAL_EVENTS_ENABLED:false}
 
 management.health.jms.enabled=false
 
diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle
index 39ee749ae..c256c2495 100644
--- a/integration-tests/build.gradle
+++ b/integration-tests/build.gradle
@@ -75,7 +75,7 @@ cargo {
             } else {
                 jvmArgs += '-Dspring.datasource.hikari.driverClassName=org.mariadb.jdbc.Driver -Dspring.datasource.hikari.jdbcUrl=jdbc:mariadb://localhost:3306/fineract_tenants -Dspring.datasource.hikari.username=root -Dspring.datasource.hikari.password=mysql -Dfineract.tenant.host=localhost -Dfineract.tenant.port=3306 -Dfineract.tenant.username=root -Dfineract.tenant.password=mysql'
             }
-            jvmArgs += ' -Dspring.profiles.active=test'
+            jvmArgs += ' -Dspring.profiles.active=test -Dfineract.events.external.enabled=true'
             property 'cargo.start.jvmargs', jvmArgs
             property 'cargo.tomcat.connector.keystoreFile', file("$rootDir/fineract-provider/src/main/resources/keystore.jks")
             property 'cargo.tomcat.connector.keystorePass', 'openmf'